const std = @import("std"); const builtin = @import("builtin"); // Single source of truth for the project version: read from build.zig.zon // and handed to the program via build_options, so `flashc --version` and the // package manifest can never drift. Mirrors the FlashOS convention. const version: []const u8 = @import("build.zig.zon").version; // Hard pin: flashc is built and run on the host toolchain, and its whole // purpose is to emit source for FlashOS, which itself pins one exact Zig. // The compiler that produces that source pins the same one. Bumping is a // deliberate act — install the new Zig, raise REQUIRED_ZIG_VERSION here and // minimum_zig_version in build.zig.zon, fix any breakage, commit. The // .zigversion file mirrors this for version managers (zigup / zvm / anyzig). const REQUIRED_ZIG_VERSION = std.SemanticVersion{ .major = 0, .minor = 16, .patch = 0 }; comptime { const v = builtin.zig_version; const r = REQUIRED_ZIG_VERSION; if (v.major != r.major or v.minor != r.minor or v.patch != r.patch) { @compileError(std.fmt.comptimePrint( "flashc requires Zig {d}.{d}.{d} exactly. Found Zig {d}.{d}.{d}. " ++ "To upgrade: bump REQUIRED_ZIG_VERSION in build.zig and " ++ "minimum_zig_version in build.zig.zon, then fix breakage.", .{ r.major, r.minor, r.patch, v.major, v.minor, v.patch }, )); } } // Build for the Flash compiler (`flashc`) — a host-native transpiler that // lowers Flash source to Zig (the "Tier 0" backend). // flashc itself is an ordinary host executable; only the code it eventually // emits is freestanding. // // Layout: // * src/main.zig — CLI driver: read .flash, run lex -> parse -> sema -> lower // * src/lexer.zig — source -> token stream (implemented) // * src/token.zig — token taxonomy // * src/parser.zig — tokens -> AST, recursive descent (implemented) // * src/ast.zig — node definitions // * src/sema.zig — native binding/scope/mutability checker (implemented) // * src/eval.zig — compile-time value/type pool (the evaluator's substrate) // * src/lower.zig — AST -> Zig source text (implemented) // * src/fmt.zig — AST -> canonical Flash source text (the formatter) // * selfhost/ — the compiler in Flash: the maintained source // (src/*.zig is the frozen stage0 bootstrap seed) // * std/ — the Flash standard library (the `core` module), // composed per stage like selfhost/ // * tools/ — host-side build tooling (the diff-corpus and // fixpoint harnesses) // // Steps: // * zig build — build flashc into zig-out/bin // * zig build run -- FILE — build then transpile FILE // * zig build test — run the host unit suite (includes test-flash, // test-selfhost, and test-std) // * zig build test-flash — transpile and run the .flash test suite // * zig build test-selfhost — transpile and run the selfhost test suite // * zig build test-std — transpile and run the std test suite // * zig build test-lsp — transpile and run the LSP server test suite // * zig build test-driver — assert the build driver's file outputs and exits // * zig build lsp — build flashd, the Flash language server // * zig build stage1 — build flashc-stage1 (stage0 transpiles selfhost/) // * zig build stage2 — build flashc-stage2 (stage1 transpiles selfhost/) // * zig build diff-corpus — assert stage0 and stage1 behave identically // * zig build fixpoint — assert stage1 and stage2 emit identical Zig pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const opts = b.addOptions(); opts.addOption([]const u8, "version", version); const exe = b.addExecutable(.{ .name = "flashc", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); exe.root_module.addOptions("build_options", opts); b.installArtifact(exe); // `zig build run -- examples/hello.flash` const run = b.addRunArtifact(exe); run.step.dependOn(b.getInstallStep()); if (b.args) |args| run.addArgs(args); const run_step = b.step("run", "Build flashc and transpile a .flash file"); run_step.dependOn(&run.step); // `zig build test` — host unit suite. Each implemented pipeline stage // carries its own `test` block next to the code it tests; the suite grows // one module at a time as the pipeline lands. Each stage's root is built // as its own test artifact; a stage's tests run together with those of // the modules it imports, so earlier stages are re-exercised through the // later ones (cheap, and a useful cross-check). const test_step = b.step("test", "Run the host unit suite"); const unit_roots = [_][]const u8{ "src/lexer.zig", "src/parser.zig", "src/sema.zig", "src/eval.zig", "src/lower.zig", "src/fmt.zig", }; for (unit_roots) |root| { const unit = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path(root), .target = b.graph.host, .optimize = .Debug, }), }); test_step.dependOn(&b.addRunArtifact(unit).step); } // Integration tests: drive the lexer over the real example sources in // examples/. The lexer and the embedded examples are wired in as modules // so the test file (in tests/) reaches them without a cross-directory // import. Grows as the pipeline lands. const lexer_mod = b.createModule(.{ .root_source_file = b.path("src/lexer.zig"), .target = b.graph.host, .optimize = .Debug, }); const examples_mod = b.createModule(.{ .root_source_file = b.path("examples/examples.zig"), .target = b.graph.host, .optimize = .Debug, }); const integration_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tests/lex_examples.zig"), .target = b.graph.host, .optimize = .Debug, .imports = &.{ .{ .name = "flash", .module = lexer_mod }, .{ .name = "examples", .module = examples_mod }, }, }), }); test_step.dependOn(&b.addRunArtifact(integration_tests).step); // fmt integration: format every example, asserting idempotence and the // comment multiset. Reuses the lexer + examples modules; `fmt` is rooted at // src/fmt.zig and pulls in the parser and lowering through its own imports. const fmt_mod = b.createModule(.{ .root_source_file = b.path("src/fmt.zig"), .target = b.graph.host, .optimize = .Debug, }); const fmt_integration_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tests/fmt_examples.zig"), .target = b.graph.host, .optimize = .Debug, .imports = &.{ .{ .name = "fmt", .module = fmt_mod }, .{ .name = "examples", .module = examples_mod }, }, }), }); test_step.dependOn(&b.addRunArtifact(fmt_integration_tests).step); // Evaluator probe corpus: every pass/ probe under tests/eval/ must check // clean, every reject/ probe must produce exactly its marked diagnostics // (the `// expect-error:` contract in tests/eval_probes.zig). The checker // is reached through a module rooted at src/sema.zig, which re-exports the // parser for exactly this wiring. const sema_mod = b.createModule(.{ .root_source_file = b.path("src/sema.zig"), .target = b.graph.host, .optimize = .Debug, }); const probes_mod = b.createModule(.{ .root_source_file = b.path("tests/eval/probes.zig"), .target = b.graph.host, .optimize = .Debug, }); const eval_probe_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tests/eval_probes.zig"), .target = b.graph.host, .optimize = .Debug, .imports = &.{ .{ .name = "sema", .module = sema_mod }, .{ .name = "probes", .module = probes_mod }, }, }), }); test_step.dependOn(&b.addRunArtifact(eval_probe_tests).step); // `zig build test-flash` — the Flash-source test suite. Each .flash file // below is transpiled by the freshly built flashc (stdout captured as a // .zig root), and the emitted `test` blocks run as ordinary Zig tests. // This is the harness Flash code tests itself with: a compiler regression // that breaks the emitted Zig fails this step, and a failing Flash-level // expectation fails the build. Also wired into `zig build test`, so the // one gate covers both suites. const flash_test_roots = [_][]const u8{ "tests/flash/basics.flash", "tests/flash/eval.flash", }; const test_flash_step = b.step("test-flash", "Transpile and run the .flash test suite"); for (flash_test_roots) |path| { const transpile = b.addRunArtifact(exe); transpile.addFileArg(b.path(path)); const root = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{std.fs.path.stem(path)}), }); const flash_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = root, .target = b.graph.host, .optimize = .Debug, }), }); test_flash_step.dependOn(&b.addRunArtifact(flash_tests).step); } test_step.dependOn(test_flash_step); // ---- the std library (the `core` module) ---- // // std/*.flash is the Flash standard library, composed exactly like the // selfhost sources: each module is transpiled into a per-stage WriteFiles // directory of its own (so std filenames can never collide with selfhost // ones), and the module rooted at that directory's core.zig is attached // to consumers as the named import "core". Stage0 (src/) never imports // core — it is the frozen bootstrap seed. std sources stay on the // bootstrap surface: their membership in the diff-corpus below enforces // that stage0 can always compile them. const std_modules = [_][]const u8{ "base", "mem", "list", "fmt", "math", "arena", "json", "core", }; // `zig build test-std` — transpile each std module with stage0 and run // its Flash `test` blocks from its place in the composed directory, where // sibling imports resolve. Wired into `zig build test`, like the others. const core_stage0_dir = b.addWriteFiles(); var core_stage0_root: ?std.Build.LazyPath = null; const test_std_step = b.step("test-std", "Transpile and run the std test suite"); for (std_modules) |name| { const transpile = b.addRunArtifact(exe); transpile.addFileArg(b.path(b.fmt("std/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); const dest = core_stage0_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "core")) core_stage0_root = dest; const std_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = dest, .target = b.graph.host, .optimize = .Debug, }), }); test_std_step.dependOn(&b.addRunArtifact(std_tests).step); } test_step.dependOn(test_std_step); // The stage0-composed core, packaged once per consumer flavour: the host // test artifacts (test-selfhost roots) and the stage1 binary. const core_host_mod = b.createModule(.{ .root_source_file = core_stage0_root.?, .target = b.graph.host, .optimize = .Debug, }); const core_stage1_mod = b.createModule(.{ .root_source_file = core_stage0_root.?, .target = target, .optimize = optimize, }); // ---- self-host scaffolding ---- // // The self-hosted compiler lives in selfhost/*.flash — the maintained // compiler source; the handwritten src/*.zig is the frozen stage0 // bootstrap seed, kept so a clean clone builds with nothing but a Zig // toolchain. `stage1` composes a source directory: each selfhost module // is transpiled by the freshly built stage0 flashc and its generated Zig // joins the directory. Relative imports resolve inside the composed // directory unchanged, so the build needs no import rewriting. `stage2` // repeats the composition with the stage1 binary as the transpiler — the // self-applied build whose output the fixpoint gate certifies. const selfhost_modules = [_][]const u8{ "support", "token", "lexer", "ast", "parser", "sema", "eval", "lower", "fmt", "main", }; // `zig build test-selfhost` — transpile each selfhost module and run its // Flash `test` blocks from its place in the composed directory, where // sibling imports resolve. Wired into `zig build test`, like test-flash. const stage1_dir = b.addWriteFiles(); var stage1_root: ?std.Build.LazyPath = null; const test_selfhost_step = b.step("test-selfhost", "Transpile and run the selfhost test suite"); for (selfhost_modules) |name| { const transpile = b.addRunArtifact(exe); transpile.addFileArg(b.path(b.fmt("selfhost/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); const dest = stage1_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "main")) stage1_root = dest; const selfhost_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = dest, .target = b.graph.host, .optimize = .Debug, }), }); // The driver reads the version through build_options, like the stage // binaries; the pipeline modules never touch it. if (std.mem.eql(u8, name, "main")) selfhost_tests.root_module.addOptions("build_options", opts); // Every root that reaches the composed support.zig must resolve // @import("core"); adding it where unused is harmless. selfhost_tests.root_module.addImport("core", core_host_mod); test_selfhost_step.dependOn(&b.addRunArtifact(selfhost_tests).step); } test_step.dependOn(test_selfhost_step); // `zig build stage1` — the compiler built from stage0-emitted Zig. const stage1_exe = b.addExecutable(.{ .name = "flashc-stage1", .root_module = b.createModule(.{ .root_source_file = stage1_root.?, .target = target, .optimize = optimize, }), }); stage1_exe.root_module.addOptions("build_options", opts); stage1_exe.root_module.addImport("core", core_stage1_mod); const stage1_step = b.step("stage1", "Build flashc-stage1 from the composed selfhost sources"); stage1_step.dependOn(&b.addInstallArtifact(stage1_exe, .{}).step); // `zig build stage2` — the compiler built from stage1-emitted Zig: the // self-hosted compiler compiling its own sources. Its core is likewise // stage1-composed: the same std sources, transpiled by stage1. const core_stage2_dir = b.addWriteFiles(); var core_stage2_root: ?std.Build.LazyPath = null; for (std_modules) |name| { const transpile = b.addRunArtifact(stage1_exe); transpile.addFileArg(b.path(b.fmt("std/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); const dest = core_stage2_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "core")) core_stage2_root = dest; } const core_stage2_mod = b.createModule(.{ .root_source_file = core_stage2_root.?, .target = target, .optimize = optimize, }); const stage2_dir = b.addWriteFiles(); var stage2_root: ?std.Build.LazyPath = null; for (selfhost_modules) |name| { const transpile = b.addRunArtifact(stage1_exe); transpile.addFileArg(b.path(b.fmt("selfhost/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); const dest = stage2_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "main")) stage2_root = dest; } const stage2_exe = b.addExecutable(.{ .name = "flashc-stage2", .root_module = b.createModule(.{ .root_source_file = stage2_root.?, .target = target, .optimize = optimize, }), }); stage2_exe.root_module.addOptions("build_options", opts); stage2_exe.root_module.addImport("core", core_stage2_mod); const stage2_step = b.step("stage2", "Build flashc-stage2 from the stage1-composed selfhost sources"); stage2_step.dependOn(&b.addInstallArtifact(stage2_exe, .{}).step); // ---- the LSP server (tools/lsp) ---- // // The language server is a standalone Flash application — the first // real Flash program outside the compiler itself. It is NOT part of // the bootstrap: its sources are transpiled by the self-hosted stage1 // binary (the shipped compiler), so they may use the full current // surface and never constrain the frozen stage0 seed. Composition // mirrors stage2: per-module transpile into one directory where // sibling imports resolve, with the stage1-emitted core attached as // the named import "core". const lsp_modules = [_][]const u8{ "transport", "check", "server", "main", }; // The selfhost frontend modules the server checks documents with — // transpiled (by stage1, like the LSP sources) into the same composed // directory so `use "parser"` resolves, but not test roots here: their // tests already run under test-selfhost. const lsp_frontend_modules = [_][]const u8{ "support", "token", "lexer", "ast", "parser", "sema", "eval", }; // `zig build test-lsp` — transpile each LSP module and run its Flash // `test` blocks. Wired into `zig build test`, like the other suites. const lsp_core_host_mod = b.createModule(.{ .root_source_file = core_stage2_root.?, .target = b.graph.host, .optimize = .Debug, }); const lsp_dir = b.addWriteFiles(); var lsp_root: ?std.Build.LazyPath = null; const test_lsp_step = b.step("test-lsp", "Transpile and run the LSP server test suite"); for (lsp_frontend_modules) |name| { const transpile = b.addRunArtifact(stage1_exe); transpile.addFileArg(b.path(b.fmt("selfhost/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); _ = lsp_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); } for (lsp_modules) |name| { const transpile = b.addRunArtifact(stage1_exe); transpile.addFileArg(b.path(b.fmt("tools/lsp/{s}.flash", .{name}))); const generated = transpile.captureStdOut(.{ .basename = b.fmt("{s}.zig", .{name}), }); const dest = lsp_dir.addCopyFile(generated, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "main")) lsp_root = dest; const lsp_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = dest, .target = b.graph.host, .optimize = .Debug, }), }); if (std.mem.eql(u8, name, "main")) lsp_tests.root_module.addOptions("build_options", opts); lsp_tests.root_module.addImport("core", lsp_core_host_mod); test_lsp_step.dependOn(&b.addRunArtifact(lsp_tests).step); } test_step.dependOn(test_lsp_step); // `zig build lsp` — build and install flashd, the language server. const lsp_exe = b.addExecutable(.{ .name = "flashd", .root_module = b.createModule(.{ .root_source_file = lsp_root.?, .target = target, .optimize = optimize, }), }); lsp_exe.root_module.addOptions("build_options", opts); lsp_exe.root_module.addImport("core", core_stage2_mod); const lsp_step = b.step("lsp", "Build flashd, the Flash language server"); lsp_step.dependOn(&b.addInstallArtifact(lsp_exe, .{}).step); // `zig build diff-corpus` — run stage0 and stage1 over every .flash file // in the corpus and assert byte-identical behaviour across transpile, // --dump-tokens, and fmt (including diagnostics and exit status on the // files the compiler rejects). This is the equality gate every hybrid // module swap must keep green. const diff_tool = b.addExecutable(.{ .name = "diff-corpus", .root_module = b.createModule(.{ .root_source_file = b.path("tools/diff_corpus.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const diff_run = b.addRunArtifact(diff_tool); diff_run.setCwd(b.path(".")); diff_run.addArtifactArg(exe); diff_run.addArtifactArg(stage1_exe); diff_run.addArgs(&.{ "examples", "selfhost", "std", "tests/eval/pass", "tests/eval/reject", "tests/flash", }); const diff_step = b.step("diff-corpus", "Assert stage0 and stage1 behave byte-identically over the corpus"); diff_step.dependOn(&diff_run.step); // `zig build fixpoint` — the bootstrap fixpoint gate: stage1 and stage2 // must emit byte-identical Zig for every selfhost module and example // (examples/register/ holds the post-v0.5 grammar samples — frozen stage0 // cannot parse them, so they ride this gate and stay out of the // stage0-facing diff-corpus above), and // each binary must emit the same bytes across two consecutive runs. When // this holds the generation chain is closed — the compiler the Flash // sources describe reproduces itself exactly, and stage2 is the fixed // point: a stage3 built from stage2's output would be compiled from the // very bytes that built stage2. const fixpoint_tool = b.addExecutable(.{ .name = "fixpoint", .root_module = b.createModule(.{ .root_source_file = b.path("tools/fixpoint.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const fixpoint_run = b.addRunArtifact(fixpoint_tool); fixpoint_run.setCwd(b.path(".")); fixpoint_run.addArtifactArg(stage1_exe); fixpoint_run.addArtifactArg(stage2_exe); fixpoint_run.addArgs(&.{ "selfhost", "std", "examples", "examples/register" }); const fixpoint_step = b.step("fixpoint", "Assert stage1 and stage2 emit byte-identical Zig, deterministically"); fixpoint_step.dependOn(&fixpoint_run.step); // `zig build test-driver` — drive the stage1 binary's file-writing modes // (`-o`, `build`) over a throwaway tree under .zig-cache and assert the // written bytes, exit codes, and flag composition. Wired into // `zig build test`, like the other suites. const driver_tool = b.addExecutable(.{ .name = "driver-test", .root_module = b.createModule(.{ .root_source_file = b.path("tools/driver_test.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const driver_run = b.addRunArtifact(driver_tool); driver_run.setCwd(b.path(".")); driver_run.addArtifactArg(stage1_exe); const test_driver_step = b.step("test-driver", "Assert the build driver's file outputs, exit codes, and flag composition"); test_driver_step.dependOn(&driver_run.step); test_step.dependOn(test_driver_step); // The COOKBOOK.md twin: transpile examples/register/cookbook.flash with // stage1 (it uses post-v0.5 grammar stage0 cannot parse) and run its // `test` blocks against the stage0-composed core, so every recipe on the // cookbook page stays code that compiles AND passes. Part of // `zig build test`; no step of its own. const cookbook_transpile = b.addRunArtifact(stage1_exe); cookbook_transpile.addFileArg(b.path("examples/register/cookbook.flash")); const cookbook_generated = cookbook_transpile.captureStdOut(.{ .basename = "cookbook.zig", }); const cookbook_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = cookbook_generated, .target = b.graph.host, .optimize = .Debug, }), }); cookbook_tests.root_module.addImport("core", core_host_mod); test_step.dependOn(&b.addRunArtifact(cookbook_tests).step); }