ajhahn.de
← Flash
Zig 5129 lines
// Flash lowering — AST to Zig source text for Flash (the Tier 0 backend).
//
// This is the whole Tier-0 bet: instead of emitting machine code, Flash emits
// Zig and lets the existing FlashOS toolchain do code generation, linking,
// +strict-align handling, and comptime. The mapping is intentionally close to
// 1:1 so a ported module is readable Zig a human can diff against the original
// during the migration:
//
//   use X            -> const X = @import("X");           (a bare module/package)
//   use X as Y       -> const Y = @import("X");
//   use "X" as Y     -> const Y = @import("X.zig");       (a sibling file import;
//                        lowering owns the backend '.zig' suffix — the Flash
//                        source names the stem only, no file extension)
//   pub use X as Y   -> pub const Y = @import("X");       (a re-exported import)
//   link "M"         -> comptime { _ = @import("M"); }   (consecutive links fold
//                                                          into one comptime block)
//   #name(a, …)      -> @name(a, …)   (a compiler intrinsic: the Flash '#'
//                        sigil maps 1:1 to Zig's '@' — the cast family
//                        #intCast/#ptrCast/#bitCast/#as/#truncate/#alignCast/
//                        #intFromPtr/#ptrFromInt each keeps its distinct
//                        semantics; the spelling is the only change)
//   fn f(a T) R      -> fn f(a: T) R { ... }   (the return type follows the
//                        parameter list directly — no arrow; a missing return
//                        lowers to void)
//   fn f(a T)        -> fn f(a: T) void { ... }
//   export fn        -> export fn ... callconv(.c)        (a C-ABI boundary)
//   inline fn        -> inline fn ...                     (always-inline marker)
//   /// text         -> /// text          (a `///` doc comment: content kept
//                        byte-for-byte and re-emitted before its declaration —
//                        const, fn, struct field/member, enum/union variant)
//   []T              -> []const T        ([]mut T -> []T)
//   [*]T             -> [*]const T       (const-pointee default; [*]mut T -> [*]T)
//   [*:s]T           -> [*:s]const T     (sentinel many-ptr; [*:s]mut T -> [*:s]T)
//   [N]T             -> [N]T
//   argv             -> [*]const ?[*:0]const u8   (builtin alias; suppressed when
//   cstr             -> [*:0]const u8              the program declares the name)
//   fn(P, …) R    -> fn (P, …) R   (a function *type*; zig fmt spaces the
//                        anonymous `fn (`; a missing return -> void)
//   *fn(P, …) R   -> *const fn (P, …) R   (a function *pointer*: `*` over the
//                        function type, const-pointee by default like any *T;
//                        *mut fn(…) R -> *fn (…) R for a mutable one)
//   const N T = e    -> const N: T = e;                   (top-level constant)
//   if c { … }       -> if (c) { … }     (else / else-if supported)
//   while c { … }    -> while (c) { … }
//   while c |x| {…}  -> while (c) |x| { … }   (optional/error payload capture)
//   for x in xs {…}  -> for (xs) |x| { … }
//   for i in a..b {} -> for (a..b) |i| { … }  (range; `for x, i in xs` indexes)
//   inline while/for -> inline while/for …    (compile-time-unrolled loops)
//   x += e           -> x += e;           (= += -= *= /= %= &= |= ^= <<= >>=
//                                          pass through)
//   break / continue -> break; / continue;
//   return e         -> return e;
//   ?T / !T          -> ?T / !T            (optional / inferred error-union types)
//   E!T              -> E!T                 (explicit error union: a named set E)
//   error.Name       -> error.Name         (an error-value origination)
//   error{ A, B }    -> error{ A, B }      (a named error set; a single member
//                        and the empty set stay tight: error{One} / error{})
//   try e            -> try e              (propagate an error union)
//   e catch h        -> e catch h          (`e catch |err| h` with a capture)
//   if opt |x| {…}   -> if (opt) |x| {…}   (optional-capture if)
//   defer s          -> defer s;           (errdefer s -> errdefer s;)
//   const N = struct {f T} -> const N = struct { f: T };   (fields, then any
//                        `fn` / `const` decls, blank-line separated like zig fmt)
//   const N = enum {a, b}  -> const N = enum { a, b };      (enum(T) + explicit
//                        `= discriminant` per variant both supported)
//   const U = union(enum) {a, b T} -> union(enum) { a, b: T };  (tagged union;
//                        optional payload type per variant, bare == void)
//   .{ .x = 1 }      -> .{ .x = 1 }         (named struct-init field)
//   .red             -> .red                (inferred enum literal)
//   p.*              -> p.*                 (single-item pointer dereference;
//                        valid as an lvalue, so `p.* = v` stores through it)
//   a[lo..hi :s]     -> a[lo..hi :s]        (sentinel-terminated slice; space
//                        before the `:`, as zig fmt lays it out)
//   const U = \\line  -> const U =\n    \\line ;   (multiline / raw string;
//                        byte-exact in const/binding/discard value position)
//
// Flash makes braces mandatory, so a single-statement `if`/`while`/`for` still
// lowers with braces. That keeps the output valid, idiomatic Zig but means a
// control-flow port is a human-diffable equivalent of the hand-written form
// rather than byte-identical to it (the straight-line coreutils stay exact).
//
// Top-level items are emitted in source order, separated by a single blank
// line, where a "unit" is a run of consecutive `use` declarations, a run of
// consecutive `link` declarations (one comptime block), one top-level constant,
// one function, or one test block. The emitted file ends with a trailing
// newline, as hand-written Zig does.

const std = @import("std");
const ast = @import("ast.zig");

pub const Error = error{OutOfMemory};

// The backend artifact suffix appended to a quoted file import: `use "syscalls"`
// lowers to @import("syscalls.zig"). Lowering owns this — frozen Flash source
// names no file extension, so the suffix changes here (not across the corpus)
// when the Tier-0 Zig backend is replaced.
const backend_ext = ".zig";

pub fn emit(arena: std.mem.Allocator, program: ast.Program) Error![]const u8 {
    var e: Emitter = .{ .arena = arena };
    const items = program.items;
    // A top-level declaration (const or var) named `argv` / `cstr` shadows the
    // builtin type alias of that name for the whole file (emitType then emits it
    // verbatim instead of expanding it). The corpus declares neither, so this
    // changes no byte.
    for (items) |it| {
        if (it == .const_decl) {
            if (std.mem.eql(u8, it.const_decl.name, "argv")) e.argv_shadowed = true;
            if (std.mem.eql(u8, it.const_decl.name, "cstr")) e.cstr_shadowed = true;
        }
    }
    var i: usize = 0;
    var first = true;
    while (i < items.len) {
        if (!first) try e.raw("\n"); // blank line between units
        first = false;
        switch (items[i]) {
            .use_decl => {
                while (i < items.len and items[i] == .use_decl) : (i += 1) {
                    try e.emitUseDeclAt(items[i].use_decl);
                    try e.raw("\n");
                }
            },
            .link_decl => {
                try e.raw("comptime {\n");
                while (i < items.len and items[i] == .link_decl) : (i += 1) {
                    try e.print("    _ = @import(\"{s}\");\n", .{items[i].link_decl.module});
                }
                try e.raw("}\n");
            },
            .const_decl => |c| {
                try e.emitConstDecl(c);
                i += 1;
            },
            .fn_decl => |f| {
                try e.emitFn(f);
                i += 1;
            },
            .comptime_block => |stmts| {
                // `comptime { … }` at file scope — the body reuses the shared
                // block layout (statements at depth 1, closing brace at 0).
                try e.raw("comptime ");
                try e.emitBlockBody(stmts, 0);
                try e.raw("\n");
                i += 1;
            },
            .test_decl => |t| {
                // `test "name" { … }` lowers one-to-one to a Zig test block;
                // the name lexeme (quotes included) re-emits verbatim.
                try e.print("test {s} ", .{t.name});
                try e.emitBlockBody(t.body, 0);
                try e.raw("\n");
                i += 1;
            },
        }
    }
    return e.buf.toOwnedSlice(arena);
}

const Emitter = struct {
    arena: std.mem.Allocator,
    buf: std.ArrayList(u8) = .empty,
    // The builtin type aliases `argv` / `cstr` (see emitType) are suppressed when
    // the program declares a top-level binding of that name, so a user — or a
    // future standard library — alias wins instead of being silently overridden.
    // Set once, before emission, from the top-level declarations.
    argv_shadowed: bool = false,
    cstr_shadowed: bool = false,

    fn raw(self: *Emitter, s: []const u8) Error!void {
        try self.buf.appendSlice(self.arena, s);
    }

    fn print(self: *Emitter, comptime fmt: []const u8, args: anytype) Error!void {
        try self.buf.print(self.arena, fmt, args);
    }

    // A top-level function: emitted at depth 0 and terminated with a newline.
    // The depth-aware body lives in emitFnAt, which a struct method reuses.
    fn emitFn(self: *Emitter, f: ast.FnDecl) Error!void {
        try self.emitDoc(f.doc, 0);
        try self.emitFnAt(f, 0);
        try self.raw("\n");
    }

    // Emit a function whose signature starts at the current column (the caller
    // supplies any leading indent) and whose closing brace returns to `depth` —
    // 0 for a top-level function, the struct's field indent for a method. The
    // body is one level deeper; no trailing newline is emitted.
    fn emitFnAt(self: *Emitter, f: ast.FnDecl, depth: usize) Error!void {
        if (f.is_pub) try self.raw("pub ");
        if (f.is_export) try self.raw("export ");
        if (f.is_extern) try self.raw("extern ");
        if (f.is_inline) try self.raw("inline ");
        try self.print("fn {s}(", .{f.name});
        for (f.params, 0..) |p, idx| {
            if (idx != 0) try self.raw(", ");
            if (p.is_comptime) try self.raw("comptime ");
            try self.raw(p.name orelse "_");
            try self.raw(": ");
            try self.emitType(p.type);
        }
        try self.raw(")");
        // An explicit `callconv(.c)` from the signature wins; otherwise an
        // `export fn` is canonicalised with the implicit C ABI marker.
        if (f.call_conv) |cc| {
            try self.raw(" callconv(");
            try self.emitExpr(cc);
            try self.raw(")");
        } else if (f.is_export) {
            try self.raw(" callconv(.c)");
        }
        try self.raw(" ");
        if (f.ret) |r| try self.emitType(r) else try self.raw("void");
        // A bodyless prototype (`extern fn`) closes with `;`; a defined
        // function emits its brace body one space after the return type.
        if (f.body) |body| {
            try self.raw(" ");
            try self.emitBlockBody(body, depth);
        } else {
            try self.raw(";");
        }
    }

    // A top-level constant: emitted at depth 0 and terminated with a newline.
    // emitConstDeclAt carries the depth so an associated constant inside a
    // struct reuses it.
    fn emitConstDecl(self: *Emitter, c: ast.ConstDecl) Error!void {
        try self.emitDoc(c.doc, 0);
        try self.emitConstDeclAt(c, 0);
        try self.raw("\n");
    }

    // Emit `const NAME[: T] = value;` starting at the current column (the caller
    // supplies any leading indent), ending at the `;` with no trailing newline.
    // `depth` is the statement's own indent, threaded into the value so a
    // multiline string or a nested type definition lays out one level deeper.
    fn emitConstDeclAt(self: *Emitter, c: ast.ConstDecl, depth: usize) Error!void {
        if (c.is_pub) try self.raw("pub ");
        try self.raw(if (c.is_mut) "var " else "const ");
        try self.raw(c.name);
        if (c.type) |ty| {
            try self.raw(": ");
            try self.emitType(ty);
        }
        if (c.value == .multiline_str) {
            try self.raw(" ");
            try self.emitMultilineRhs(c.value.multiline_str, depth);
        } else {
            try self.raw(" = ");
            try self.emitValue(c.value, depth);
            try self.raw(";");
        }
    }

    // Emit one import as `[pub ]const NAME = @import("TARGET");` at the current
    // column (the caller supplies any leading indent), ending at the `;` with no
    // trailing newline. A quoted file import names the module stem only — lowering
    // appends the backend artifact suffix here — so the same `use` form lowers
    // identically at the top level and inside a struct body.
    fn emitUseDeclAt(self: *Emitter, u: ast.UseDecl) Error!void {
        const vis = if (u.is_pub) "pub " else "";
        const name = u.alias orelse u.module;
        if (u.is_file) {
            try self.print("{s}const {s} = @import(\"{s}{s}\");", .{ vis, name, u.module, backend_ext });
        } else {
            try self.print("{s}const {s} = @import(\"{s}\");", .{ vis, name, u.module });
        }
    }

    // Lay out a multiline-string value in assignment-RHS position. The caller
    // has emitted the left-hand side and a trailing space, up to but not
    // including the `=`. Produces, matching zig fmt:
    //     =\n  <\\lines at depth+1>  indent(depth) ;
    // `depth` is the statement's own indent (0 for a top-level constant).
    fn emitMultilineRhs(self: *Emitter, lines: []const []const u8, depth: usize) Error!void {
        try self.raw("=\n");
        for (lines) |ln| {
            try self.indent(depth + 1);
            try self.raw("\\\\");
            try self.raw(ln);
            try self.raw("\n");
        }
        try self.indent(depth);
        try self.raw(";");
    }

    // Emit the value of a binding or constant. A struct/enum type definition
    // lays out across multiple lines with its closing brace at `depth` (the
    // statement's own indent); every other value is a single-line expression.
    fn emitValue(self: *Emitter, value: ast.Expr, depth: usize) Error!void {
        switch (value) {
            .struct_def, .enum_def, .union_def => try self.emitTypeDef(value, depth),
            else => try self.emitExprAt(value, depth),
        }
    }

    // Lay out a `struct { … }` / `enum { … }` type definition. Fields/variants
    // sit one per line at `depth + 1` with a trailing comma (the zig fmt form);
    // the closing brace returns to `depth`. The caller supplies the opening
    // `const Name = ` and the closing `;`.
    fn emitTypeDef(self: *Emitter, x: ast.Expr, depth: usize) Error!void {
        switch (x) {
            .struct_def => |sd| {
                // A container with no members at all is zig fmt's one-line
                // form: `struct {}`.
                if (sd.fields.len == 0 and sd.decls.len == 0) {
                    try self.raw("struct {}");
                    return;
                }
                try self.raw("struct {\n");
                for (sd.fields) |f| {
                    try self.emitDoc(f.doc, depth + 1);
                    try self.indent(depth + 1);
                    try self.raw(f.name);
                    try self.raw(": ");
                    try self.emitType(f.type);
                    // A default value renders after the type: `name: T = expr,`.
                    if (f.default) |d| {
                        try self.raw(" = ");
                        try self.emitExpr(d);
                    }
                    try self.raw(",\n");
                }
                try self.emitContainerDecls(sd.decls, sd.fields.len != 0, depth);
                try self.indent(depth);
                try self.raw("}");
            },
            .enum_def => |ed| {
                try self.raw("enum");
                if (ed.tag_type) |t| {
                    try self.raw("(");
                    try self.raw(t);
                    try self.raw(")");
                }
                if (ed.variants.len == 0 and ed.decls.len == 0) {
                    try self.raw(" {}");
                    return;
                }
                try self.raw(" {\n");
                for (ed.variants) |v| {
                    try self.emitDoc(v.doc, depth + 1);
                    try self.indent(depth + 1);
                    try self.raw(v.name);
                    if (v.value) |val| {
                        try self.raw(" = ");
                        try self.emitExpr(val.*);
                    }
                    try self.raw(",\n");
                }
                try self.emitContainerDecls(ed.decls, ed.variants.len != 0, depth);
                try self.indent(depth);
                try self.raw("}");
            },
            // `union(enum) { … }` — like the enum layout, but each variant may
            // carry a payload type (`name: T`); a bare name is a void variant.
            .union_def => |ud| {
                try self.raw("union");
                if (ud.tag) |t| {
                    try self.raw("(");
                    try self.raw(t);
                    try self.raw(")");
                }
                if (ud.variants.len == 0 and ud.decls.len == 0) {
                    try self.raw(" {}");
                    return;
                }
                try self.raw(" {\n");
                for (ud.variants) |v| {
                    try self.emitDoc(v.doc, depth + 1);
                    try self.indent(depth + 1);
                    try self.raw(v.name);
                    if (v.payload) |ty| {
                        try self.raw(": ");
                        try self.emitType(ty);
                    }
                    try self.raw(",\n");
                }
                try self.emitContainerDecls(ud.decls, ud.variants.len != 0, depth);
                try self.indent(depth);
                try self.raw("}");
            },
            else => unreachable,
        }
    }

    // Associated declarations follow a container's fields/variants, each
    // preceded by a blank line (one after the member block, one between decls) —
    // the idiomatic container layout zig fmt preserves. A container whose first
    // member is a declaration gets no leading blank.
    fn emitContainerDecls(self: *Emitter, decls: []const ast.ContainerDecl, has_members: bool, depth: usize) Error!void {
        for (decls, 0..) |d, idx| {
            if (idx != 0 or has_members) try self.raw("\n");
            // The decl's doc block precedes its own indented signature line.
            switch (d) {
                .method => |m| {
                    try self.emitDoc(m.doc, depth + 1);
                    try self.indent(depth + 1);
                    try self.emitFnAt(m, depth + 1);
                },
                .constant => |c| {
                    try self.emitDoc(c.doc, depth + 1);
                    try self.indent(depth + 1);
                    try self.emitConstDeclAt(c, depth + 1);
                },
                .use_import => |u| {
                    // No doc (the parser forbids it); a container-level import
                    // lowers exactly like a top-level one, just indented.
                    try self.indent(depth + 1);
                    try self.emitUseDeclAt(u);
                },
            }
            try self.raw("\n");
        }
    }

    // Emit a brace-delimited block body, opening at the current column. An empty
    // statement list collapses to `{}` on one line (matching zig fmt); a
    // non-empty one opens `{`, lays out one statement per line at `depth + 1`,
    // and closes `}` back at `depth`. The single source of the empty-block rule —
    // every block-emitting site (function body, `if`/`else`, `while`, `for`)
    // routes through here, so the collapse is consistent. The caller supplies any
    // leading space before the `{`; no trailing newline is emitted.
    fn emitBlockBody(self: *Emitter, stmts: []const ast.Stmt, depth: usize) Error!void {
        if (stmts.len == 0) {
            try self.raw("{}");
            return;
        }
        try self.raw("{\n");
        try self.emitBlock(stmts, depth + 1);
        try self.indent(depth);
        try self.raw("}");
    }

    // Emit a block's statements, each on its own line at the given indent depth
    // (depth 1 == one level inside a function body). Block-structured statements
    // recurse at depth + 1 for their inner statements.
    fn emitBlock(self: *Emitter, stmts: []const ast.Stmt, depth: usize) Error!void {
        for (stmts) |s| {
            try self.indent(depth);
            try self.emitStmt(s, depth);
            try self.raw("\n");
        }
    }

    fn indent(self: *Emitter, depth: usize) Error!void {
        var k: usize = 0;
        while (k < depth) : (k += 1) try self.raw("    ");
    }

    // Emit a `///` doc-comment block: one line per entry at `depth`, each the
    // three slashes plus its preserved content. The caller emits it immediately
    // before the declaration's own indented line, where zig fmt keeps it
    // byte-for-byte. An empty `doc` emits nothing.
    fn emitDoc(self: *Emitter, doc: []const []const u8, depth: usize) Error!void {
        for (doc) |line| {
            try self.indent(depth);
            try self.raw("///");
            try self.raw(line);
            try self.raw("\n");
        }
    }

    fn emitType(self: *Emitter, t: ast.TypeRef) Error!void {
        switch (t) {
            .name => |n| {
                // `argv` and `cstr` are builtin spelling aliases for the two
                // pointer types the coreutils need but the surface gives no syntax
                // for. They are suppressed when the program declares a top-level
                // binding of the same name (argv_shadowed / cstr_shadowed), so a
                // user — or a future standard library — alias wins instead of
                // being silently overridden; the name then lowers verbatim.
                if (!self.argv_shadowed and std.mem.eql(u8, n, "argv")) {
                    try self.raw("[*]const ?[*:0]const u8");
                } else if (!self.cstr_shadowed and std.mem.eql(u8, n, "cstr")) {
                    try self.raw("[*:0]const u8");
                } else {
                    try self.raw(n);
                }
            },
            .slice => |inner| {
                try self.raw("[]const ");
                try self.emitType(inner.*);
            },
            .slice_mut => |inner| {
                try self.raw("[]");
                try self.emitType(inner.*);
            },
            .slice_sentinel => |sp| {
                // `[:s]const T` — the sentinel-terminated slice; const-pointee by
                // default like a plain `[]T`. The sentinel sits between `:` and `]`.
                try self.raw("[:");
                try self.emitExpr(sp.sentinel.*);
                try self.raw("]const ");
                try self.emitType(sp.elem.*);
            },
            .slice_sentinel_mut => |sp| {
                // `[:s]T` — its mutable form (no `const`), as `[]mut T` is to `[]T`.
                try self.raw("[:");
                try self.emitExpr(sp.sentinel.*);
                try self.raw("]");
                try self.emitType(sp.elem.*);
            },
            .many_ptr => |inner| {
                // `[*]T` is const-pointee by default, like `[]T` and `*T`; the
                // `const` is implicit in the Flash surface and explicit in Zig,
                // spaced from the element type as zig fmt lays it out.
                try self.raw("[*]const ");
                try self.emitType(inner.*);
            },
            .many_ptr_mut => |inner| {
                // `[*]mut T` — a writable many-item pointer; no `const` in Zig.
                try self.raw("[*]");
                try self.emitType(inner.*);
            },
            .many_ptr_volatile => |inner| {
                // `[*]volatile T` — const+volatile pointee, const by default like
                // every pointer family; the implicit `const` renders before
                // `volatile`, both spaced from the element type as zig fmt does.
                try self.raw("[*]const volatile ");
                try self.emitType(inner.*);
            },
            .many_ptr_mut_volatile => |inner| {
                // `[*]mut volatile T` — the writable+volatile form; no `const`.
                try self.raw("[*]volatile ");
                try self.emitType(inner.*);
            },
            .many_ptr_sentinel => |sp| {
                // `[*:s]T` — const-pointee by default like `[*]T`; the sentinel
                // expr is kept verbatim and the implicit `const` renders after the
                // `]`, before the element type.
                try self.raw("[*:");
                try self.emitExpr(sp.sentinel.*);
                try self.raw("]const ");
                try self.emitType(sp.elem.*);
            },
            .many_ptr_sentinel_mut => |sp| {
                // `[*:s]mut T` — the writable sentinel form; no `const` in Zig.
                try self.raw("[*:");
                try self.emitExpr(sp.sentinel.*);
                try self.raw("]");
                try self.emitType(sp.elem.*);
            },
            .ptr => |inner| {
                // `*T` is const-pointee by default, like `[]T` is a const slice.
                try self.raw("*const ");
                try self.emitType(inner.*);
            },
            .ptr_mut => |inner| {
                try self.raw("*");
                try self.emitType(inner.*);
            },
            .ptr_volatile => |inner| {
                // `*volatile T` — const+volatile pointee, const by default; the
                // implicit `const` renders before `volatile`, as zig fmt lays it out.
                try self.raw("*const volatile ");
                try self.emitType(inner.*);
            },
            .ptr_mut_volatile => |inner| {
                // `*mut volatile T` — the writable+volatile form; no `const`.
                try self.raw("*volatile ");
                try self.emitType(inner.*);
            },
            .array => |arr| {
                try self.raw("[");
                try self.emitExpr(arr.len.*);
                try self.raw("]");
                try self.emitType(arr.elem.*);
            },
            .array_sentinel => |a| {
                // `[N:s]T` — a fixed-length sentinel-terminated array; length and
                // sentinel render verbatim, no spaces around the `:`, as zig fmt
                // lays it out.
                try self.raw("[");
                try self.emitExpr(a.len.*);
                try self.raw(":");
                try self.emitExpr(a.sentinel.*);
                try self.raw("]");
                try self.emitType(a.elem.*);
            },
            .array_inferred => |elem| {
                // `[_]T` — the inferred-length array; the `_` lowers verbatim.
                try self.raw("[_]");
                try self.emitType(elem.*);
            },
            .array_inferred_sentinel => |sp| {
                // `[_:s]T` — the inferred-length sentinel array (`[_:0]u8{ … }`).
                try self.raw("[_:");
                try self.emitExpr(sp.sentinel.*);
                try self.raw("]");
                try self.emitType(sp.elem.*);
            },
            .optional => |inner| {
                try self.raw("?");
                try self.emitType(inner.*);
            },
            .errunion => |eu| {
                // `E!T` names the error set, `!T` (set == null) infers it. The
                // set renders before the `!`, the payload after — both verbatim.
                if (eu.set) |s| try self.emitType(s.*);
                try self.raw("!");
                try self.emitType(eu.payload.*);
            },
            .fn_type => |ft| {
                // `fn(P, …) R` — a function type. zig fmt writes the anonymous
                // form with a space after `fn` (unlike a named `fn name(`), and a
                // missing return lowers to `void`. Any `*`/`*mut` around it is the
                // surrounding `.ptr`/`.ptr_mut` case, so this emits the bare
                // function type only — `*fn(…)` becomes `*const fn (…) R` for free.
                try self.raw("fn (");
                for (ft.params, 0..) |p, idx| {
                    if (idx != 0) try self.raw(", ");
                    try self.emitType(p);
                }
                try self.raw(") ");
                if (ft.ret) |r| try self.emitType(r.*) else try self.raw("void");
            },
            .generic => |g| {
                // `Name(args…)` — a generic applied in type position lowers as the
                // verbatim call zig reads it: the name, then the argument
                // expressions comma-separated, exactly as a value-position call.
                try self.raw(g.name);
                try self.raw("(");
                for (g.args, 0..) |arg, idx| {
                    if (idx != 0) try self.raw(", ");
                    try self.emitExpr(arg);
                }
                try self.raw(")");
            },
            .tuple => |elems| {
                // `(A, B)` — a tuple type lowers to Zig's inline positional
                // struct, spaced as zig fmt lays the one-line form out:
                // `struct { A, B }`.
                try self.raw("struct { ");
                for (elems, 0..) |e, idx| {
                    if (idx != 0) try self.raw(", ");
                    try self.emitType(e);
                }
                try self.raw(" }");
            },
        }
    }

    fn emitStmt(self: *Emitter, s: ast.Stmt, depth: usize) Error!void {
        switch (s) {
            .discard => |x| {
                if (x == .multiline_str) {
                    try self.raw("_ ");
                    try self.emitMultilineRhs(x.multiline_str, depth);
                } else {
                    try self.raw("_ = ");
                    try self.emitExprAt(x, depth);
                    try self.raw(";");
                }
            },
            .bind => |b| {
                // A `comptime var` keeps Zig's `comptime` prefix. A `comptime
                // const` cannot: Zig rejects `comptime const` as redundant and
                // directs the comptime-ness onto the initializer instead, so it
                // lowers to `const x = comptime e` — the force-comptime intent
                // rides the value, not a redundant binding keyword.
                const force_value_comptime = b.is_comptime and !b.is_mut;
                if (b.is_comptime and b.is_mut) try self.raw("comptime ");
                try self.raw(if (b.is_mut) "var " else "const ");
                try self.raw(b.name);
                if (b.type) |ty| {
                    try self.raw(": ");
                    try self.emitType(ty);
                }
                // `align(expr)` sits after the type, before `=` (Zig order).
                if (b.align_expr) |ae| {
                    try self.raw(" align(");
                    try self.emitExpr(ae);
                    try self.raw(")");
                }
                if (b.value == .multiline_str) {
                    // A multiline string literal is already comptime-known, so a
                    // `comptime const` over one needs no `comptime` wrap.
                    try self.raw(" ");
                    try self.emitMultilineRhs(b.value.multiline_str, depth);
                } else {
                    try self.raw(" = ");
                    if (force_value_comptime) try self.raw("comptime ");
                    try self.emitValue(b.value, depth);
                    try self.raw(";");
                }
            },
            .assign => |a| {
                try self.emitExprAt(a.target, depth);
                try self.raw(" ");
                try self.raw(a.op); // "=", "+=", … are spelled the same in Zig
                try self.raw(" ");
                try self.emitExprAt(a.value, depth);
                try self.raw(";");
            },
            // A destructuring bind repeats the binding keyword per name —
            // Zig's native spelling: `const a, const b = e;` / `var a, var b
            // = e;` — and a `_` skip stays `_` (Zig's discard position).
            .destructure => |d| {
                for (d.names, 0..) |maybe, i| {
                    if (i != 0) try self.raw(", ");
                    if (maybe) |name| {
                        try self.raw(if (d.is_mut) "var " else "const ");
                        try self.raw(name);
                    } else {
                        try self.raw("_");
                    }
                }
                try self.raw(" = ");
                try self.emitValue(d.value, depth);
                try self.raw(";");
            },
            // A destructuring assignment lowers verbatim: `a, b = e;`.
            .destructure_assign => |da| {
                for (da.targets, 0..) |t, i| {
                    if (i != 0) try self.raw(", ");
                    try self.emitExprAt(t, depth);
                }
                try self.raw(" = ");
                try self.emitExprAt(da.value, depth);
                try self.raw(";");
            },
            .if_stmt => |iff| try self.emitIf(iff, depth),
            // `defer` / `errdefer` prefix their inner statement; the inner
            // statement emits its own trailing `;`.
            .defer_stmt => |inner| {
                try self.raw("defer ");
                try self.emitStmt(inner.*, depth);
            },
            .errdefer_stmt => |inner| {
                try self.raw("errdefer ");
                try self.emitStmt(inner.*, depth);
            },
            // The block forms render the brace body; like an `if` body, the
            // closing `}` carries no `;`.
            .defer_block => |stmts| {
                try self.raw("defer ");
                try self.emitBlockBody(stmts, depth);
            },
            .errdefer_block => |stmts| {
                try self.raw("errdefer ");
                try self.emitBlockBody(stmts, depth);
            },
            .while_stmt => |w| {
                if (w.is_inline) try self.raw("inline ");
                try self.raw("while (");
                try self.emitExprAt(w.cond, depth);
                try self.raw(")");
                // `|x|` — the optional / error payload capture: `while (e) |x| {…}`.
                if (w.capture) |cap| {
                    try self.raw(" |");
                    try self.raw(cap);
                    try self.raw("|");
                }
                try self.raw(" ");
                try self.emitBlockBody(w.body, depth);
                // The loop else arm — ` else { … }`, the error capture rendered
                // as ` else |err| { … }`.
                if (w.else_body) |eb| {
                    try self.raw(" else ");
                    if (w.else_capture) |cap| {
                        try self.raw("|");
                        try self.raw(cap);
                        try self.raw("| ");
                    }
                    try self.emitBlockBody(eb, depth);
                }
            },
            .for_stmt => |fr| {
                if (fr.is_inline) try self.raw("inline ");
                try self.raw("for (");
                try self.emitExprAt(fr.iter, depth);
                // A range iterator prints its `..hi` bound; a second capture
                // adds the implicit index range `0..` as a parallel input.
                if (fr.range_hi) |hi| {
                    try self.raw("..");
                    try self.emitExprAt(hi, depth);
                }
                if (fr.captures.len == 2) try self.raw(", 0..");
                try self.raw(") |");
                for (fr.captures, 0..) |c, i| {
                    if (i != 0) try self.raw(", ");
                    try self.raw(c);
                }
                try self.raw("| ");
                try self.emitBlockBody(fr.body, depth);
                // The loop else arm — runs when the iteration completes
                // without `break`; capture-less.
                if (fr.else_body) |eb| {
                    try self.raw(" else ");
                    try self.emitBlockBody(eb, depth);
                }
            },
            .expr => |x| {
                try self.emitExprAt(x, depth);
                // A block-form expression used as a bare statement (a `switch`
                // or a labeled block whose value is discarded) is a complete
                // statement in Zig and takes no trailing `;`; every other
                // expression statement closes with one.
                if (!isBlockFormStmt(x)) try self.raw(";");
            },
        }
    }

    // Whether an expression, used as a bare statement, is a block-form that
    // closes on `}` and so takes no trailing `;` (Zig's BlockExpr statement).
    fn isBlockFormStmt(x: ast.Expr) bool {
        return switch (x) {
            .switch_expr, .block_expr => true,
            else => false,
        };
    }

    // `if (cond) { … }`, with an `else { … }` arm or, when the else body is
    // exactly one nested if, an idiomatic `else if (…) { … }` chain.
    fn emitIf(self: *Emitter, iff: ast.If, depth: usize) Error!void {
        try self.raw("if (");
        try self.emitExprAt(iff.cond, depth);
        try self.raw(")");
        // An optional-capture if renders the payload binding: `if (opt) |x| { … }`.
        if (iff.capture) |cap| {
            try self.raw(" |");
            try self.raw(cap);
            try self.raw("|");
        }
        try self.raw(" ");
        try self.emitBlockBody(iff.body, depth);
        if (iff.else_body) |eb| {
            if (eb.len == 1 and eb[0] == .if_stmt) {
                try self.raw(" else ");
                try self.emitIf(eb[0].if_stmt, depth);
            } else {
                // ` else { … }`, the error capture rendered as ` else |err| { … }`.
                try self.raw(" else ");
                if (iff.else_capture) |cap| {
                    try self.raw("|");
                    try self.raw(cap);
                    try self.raw("| ");
                }
                try self.emitBlockBody(eb, depth);
            }
        }
    }

    // The depth-0 wrapper, for the inline-only callers (type length / sentinel
    // expressions, struct-field and enum-variant defaults) where an expression
    // never spans multiple lines.
    fn emitExpr(self: *Emitter, x: ast.Expr) Error!void {
        try self.emitExprAt(x, 0);
    }

    // Emit an expression whose indentation is `depth`. Most expression forms are
    // single-line and ignore `depth`, threading it unchanged through their
    // sub-expressions; the multi-line forms — a labeled block, and the `switch`
    // expression — lay their inner statements / prongs out at `depth + 1` and
    // close at `depth`, so a `return switch (…) { … }` or a `blk: { … }` prong
    // body indents correctly however deeply it nests.
    fn emitExprAt(self: *Emitter, x: ast.Expr, depth: usize) Error!void {
        switch (x) {
            .int, .float, .string, .char, .ident, .value_word => |s| try self.raw(s),
            .multiline_str => |lines| {
                // Reached only outside a const/binding/discard value (e.g. a call
                // argument or `return`), where zig fmt's layout is
                // position-specific. Indentation before `\\` does not affect the
                // value, so this stays a semantically identical program; the
                // byte-exact layout is guaranteed for the routed value positions,
                // not here.
                try self.raw("\n");
                for (lines) |ln| {
                    try self.raw("\\\\");
                    try self.raw(ln);
                    try self.raw("\n");
                }
            },
            .member => |m| {
                try self.emitExprAt(m.base.*, depth);
                try self.raw(".");
                try self.raw(m.field);
            },
            .deref => |d| {
                // `p.*` — a single-item pointer dereference, spelled identically
                // in Zig; valid in value and lvalue (assignment target) position.
                try self.emitExprAt(d.*, depth);
                try self.raw(".*");
            },
            .optional_unwrap => |u| {
                // `opt.?` — optional unwrap (assert non-null), spelled identically
                // in Zig; sits in the same postfix slot as `.*`.
                try self.emitExprAt(u.*, depth);
                try self.raw(".?");
            },
            .call => |c| {
                try self.emitExprAt(c.callee.*, depth);
                try self.emitArgs(c.args, depth);
            },
            .index => |ix| {
                try self.emitExprAt(ix.base.*, depth);
                try self.raw("[");
                try self.emitExprAt(ix.index.*, depth);
                try self.raw("]");
            },
            .slice => |s| {
                try self.emitExprAt(s.base.*, depth);
                try self.raw("[");
                try self.emitExprAt(s.lo.*, depth);
                // zig fmt spaces the `..` when either bound is a binary
                // operation (`a[i .. j + 1]`); simple bounds — idents, literals,
                // calls, indexing, member access, deref — stay tight (`a[i..j]`).
                // The trailing space is emitted only when a high bound follows,
                // so an open-ended `a[i + 1 ..]` keeps the space only before `..`.
                const spaced = sliceBoundSpaces(s.lo.*) or
                    (s.hi != null and sliceBoundSpaces(s.hi.?.*));
                if (spaced) try self.raw(" ");
                try self.raw("..");
                if (s.hi) |hi| {
                    if (spaced) try self.raw(" ");
                    try self.emitExprAt(hi.*, depth);
                }
                // `[lo..hi :s]` — zig fmt puts a space before the sentinel `:`
                // and none after; an open-ended `[lo.. :s]` keeps the same form.
                if (s.sentinel) |sen| {
                    try self.raw(" :");
                    try self.emitExprAt(sen.*, depth);
                }
                try self.raw("]");
            },
            .builtin_call => |b| {
                // The AST holds the bare intrinsic name (the Flash '#' sigil is
                // stripped in the parser); Tier 0 emits Zig's '@'-prefixed form.
                try self.raw("@");
                try self.raw(b.name);
                try self.emitArgs(b.args, depth);
            },
            .unary => |u| {
                try self.raw(u.op);
                try self.emitExprAt(u.operand.*, depth);
            },
            .binary => |b| {
                try self.emitExprAt(b.lhs.*, depth);
                try self.raw(" ");
                try self.raw(zigBinOp(b.op));
                try self.raw(" ");
                try self.emitExprAt(b.rhs.*, depth);
            },
            .struct_lit => |fields| {
                // zig fmt spaces the braces (`.{ … }`) for every literal except
                // the empty `.{}` and a single positional element (`.{x}`); a
                // single named field is a struct init and stays spaced.
                const spaced = !(fields.len == 0 or (fields.len == 1 and fields[0].name == null));
                try self.raw(if (spaced) ".{ " else ".{");
                for (fields, 0..) |f, idx| {
                    if (idx != 0) try self.raw(", ");
                    if (f.name) |n| {
                        try self.raw(".");
                        try self.raw(n);
                        try self.raw(" = ");
                    }
                    try self.emitExprAt(f.value, depth);
                }
                try self.raw(if (spaced) " }" else "}");
            },
            // `Type{ .x = 1 }` — a typed initializer. The type prefix renders
            // first, then the field list with the same brace-spacing rule as the
            // anonymous `.{ … }` form (spaced unless empty / single positional).
            .typed_lit => |tl| {
                try self.emitExprAt(tl.type.*, depth);
                const spaced = !(tl.fields.len == 0 or (tl.fields.len == 1 and tl.fields[0].name == null));
                try self.raw(if (spaced) "{ " else "{");
                for (tl.fields, 0..) |f, idx| {
                    if (idx != 0) try self.raw(", ");
                    if (f.name) |n| {
                        try self.raw(".");
                        try self.raw(n);
                        try self.raw(" = ");
                    }
                    try self.emitExprAt(f.value, depth);
                }
                try self.raw(if (spaced) " }" else "}");
            },
            // A type in value position (the head of an array-typed literal,
            // `[_]u8{ … }`); the type renders through the shared type emitter.
            .type_lit => |t| try self.emitType(t.*),
            .enum_lit => |v| {
                try self.raw(".");
                try self.raw(v);
            },
            // `error.Name` — an error-value origination, spelled identically in Zig.
            .error_lit => |n| {
                try self.raw("error.");
                try self.raw(n);
            },
            // `error{ A, B }` — a named error-set definition. zig fmt spaces the
            // braces only for two-or-more members (`error{ A, B }`); a single
            // member (`error{One}`) and the empty set (`error{}`) stay tight.
            .error_set => |names| {
                const spaced = names.len > 1;
                try self.raw(if (spaced) "error{ " else "error{");
                for (names, 0..) |n, idx| {
                    if (idx != 0) try self.raw(", ");
                    try self.raw(n);
                }
                try self.raw(if (spaced) " }" else "}");
            },
            // A struct/enum/union definition appearing mid-expression — e.g. the
            // arms of a `const X = if (cond) struct {…} else struct {…}` driver
            // select. It renders at the depth threaded in from the enclosing
            // expression (0 for a top-level const), so the body indents one level
            // past that and the closing brace returns to it.
            .struct_def, .enum_def, .union_def => try self.emitTypeDef(x, depth),
            .group => |g| {
                try self.raw("(");
                try self.emitExprAt(g.*, depth);
                try self.raw(")");
            },
            // `if cond a else b` — the value form. The condition is wrapped in
            // parentheses (as zig fmt requires); the arms render inline with a
            // single space around `else`: `if (cond) a else b`.
            .if_expr => |iff| {
                try self.raw("if (");
                try self.emitExprAt(iff.cond.*, depth);
                try self.raw(") ");
                try self.emitExprAt(iff.then.*, depth);
                try self.raw(" else ");
                try self.emitExprAt(iff.else_.*, depth);
            },
            // `switch (subject) { … }` — the subject is parenthesised; prongs lay
            // out one per line at depth + 1 (`patterns => body,`) and the closing
            // brace returns to depth. A prong body is an expression at the prong's
            // own depth, so a `label: { … }` arm indents its statements correctly.
            .switch_expr => |sw| {
                try self.raw("switch (");
                try self.emitExprAt(sw.subject.*, depth);
                try self.raw(") {\n");
                for (sw.prongs) |prong| {
                    try self.indent(depth + 1);
                    if (prong.is_else) {
                        try self.raw("else");
                    } else {
                        for (prong.patterns, 0..) |p, idx| {
                            if (idx != 0) try self.raw(", ");
                            try self.emitExprAt(p.lo, depth + 1);
                            // An inclusive range `lo...hi` — no spaces, as zig fmt.
                            if (p.hi) |hi| {
                                try self.raw("...");
                                try self.emitExprAt(hi, depth + 1);
                            }
                        }
                    }
                    try self.raw(" => ");
                    // A `=> |x|` payload capture renders before the body.
                    if (prong.capture) |cap| {
                        try self.raw("|");
                        try self.raw(cap);
                        try self.raw("| ");
                    }
                    try self.emitExprAt(prong.body, depth + 1);
                    try self.raw(",\n");
                }
                try self.indent(depth);
                try self.raw("}");
            },
            // A block expression. A labelled `label: { … }` prefixes the block
            // body with its label (its value comes from a `break :label v`
            // inside); an unlabelled block — a switch-prong `=> { … }` arm — emits
            // the body alone. Statements lay out at depth + 1, brace back at depth.
            .block_expr => |blk| {
                if (blk.label) |label| {
                    try self.raw(label);
                    try self.raw(": ");
                }
                try self.emitBlockBody(blk.body, depth);
            },
            .try_expr => |t| {
                try self.raw("try ");
                try self.emitExprAt(t.*, depth);
            },
            .catch_expr => |c| {
                try self.emitExprAt(c.lhs.*, depth);
                try self.raw(" catch ");
                if (c.capture) |cap| {
                    try self.raw("|");
                    try self.raw(cap);
                    try self.raw("| ");
                }
                try self.emitExprAt(c.handler.*, depth);
            },
            .asm_expr => |a| try self.emitAsm(a, depth),
            // `break`, optionally to a labelled block (`break :blk`) and/or with a
            // value (`break :blk v`). zig fmt spaces both: `break :blk v`.
            .brk => |b| {
                try self.raw("break");
                if (b.label) |l| {
                    try self.raw(" :");
                    try self.raw(l);
                }
                if (b.value) |v| {
                    try self.raw(" ");
                    try self.emitExprAt(v.*, depth);
                }
            },
            .cont => try self.raw("continue"),
            .ret => |maybe| {
                try self.raw("return");
                if (maybe) |vals| {
                    try self.raw(" ");
                    if (vals.len == 1) {
                        try self.emitExprAt(vals[0], depth);
                    } else {
                        // `return a, b` — the multi-return sugar: the value
                        // list lowers to one anonymous tuple literal,
                        // `return .{ a, b };`, zig fmt's one-line layout.
                        try self.raw(".{ ");
                        for (vals, 0..) |v, idx| {
                            if (idx != 0) try self.raw(", ");
                            try self.emitExprAt(v, depth);
                        }
                        try self.raw(" }");
                    }
                }
            },
        }
    }

    fn emitArgs(self: *Emitter, args: []const ast.Expr, depth: usize) Error!void {
        try self.raw("(");
        for (args, 0..) |a, idx| {
            if (idx != 0) try self.raw(", ");
            try self.emitExprAt(a, depth);
        }
        try self.raw(")");
    }

    // `asm [volatile] (…)` — inline assembly, laid out exactly as zig fmt renders
    // it. The closing `)` is emitted here; the surrounding statement supplies the
    // `;`. zig fmt keeps the expression on one line when it has no output and no
    // input operands and a single-string template — a bare `asm (T)` or a
    // clobber-only `asm (T ::: C)`; any output/input operand, or a `\\` multiline
    // template, breaks it across lines. The colon sections are positional, so an
    // empty earlier section still occupies its own `:` line, and the trailing
    // clobber expression hugs the closing `)` (`: C)`), whereas an output/input
    // last section closes the `)` on its own line at the statement's depth.
    fn emitAsm(self: *Emitter, a: ast.AsmExpr, depth: usize) Error!void {
        try self.raw("asm ");
        if (a.is_volatile) try self.raw("volatile ");
        try self.raw("(");

        const ml_template = a.template.* == .multiline_str;
        const multiline = ml_template or a.outputs.len > 0 or a.inputs.len > 0;

        if (!multiline) {
            try self.emitExprAt(a.template.*, depth);
            if (a.clobbers) |c| {
                try self.raw(" ::: ");
                try self.emitExprAt(c.*, depth);
            }
            try self.raw(")");
            return;
        }

        // Multi-line: the template heads its own line(s), the sections follow.
        if (ml_template) {
            try self.raw("\n");
            for (a.template.*.multiline_str) |ln| {
                try self.indent(depth + 1);
                try self.raw("\\\\");
                try self.raw(ln);
                try self.raw("\n");
            }
        } else {
            try self.emitExprAt(a.template.*, depth);
            try self.raw("\n");
        }

        // The highest non-empty section fixes how many positional colons appear:
        // a clobber forces all three, an input forces outputs+inputs, an output
        // forces outputs alone. A multiline template with no operands has none.
        const n_sections: usize = if (a.clobbers != null)
            3
        else if (a.inputs.len > 0)
            2
        else if (a.outputs.len > 0)
            1
        else
            0;

        if (n_sections >= 1) {
            try self.indent(depth + 1);
            try self.raw(":");
            try self.emitAsmOperandList(a.outputs, depth);
        }
        if (n_sections >= 2) {
            try self.indent(depth + 1);
            try self.raw(":");
            try self.emitAsmOperandList(a.inputs, depth);
        }
        if (a.clobbers) |c| {
            try self.indent(depth + 1);
            try self.raw(": ");
            try self.emitExprAt(c.*, depth);
            try self.raw(")");
            return;
        }
        // No clobber section: the last operand carried a trailing comma + newline,
        // so the `)` closes on a fresh line at the statement's own depth.
        try self.indent(depth);
        try self.raw(")");
    }

    // The operands of one asm section. The first follows its colon on the same
    // line (`: op,`); each later one sits on its own line aligned two columns
    // past the colon. Every operand carries a trailing comma. An empty section
    // is just its colon, so the line is closed with a bare newline.
    fn emitAsmOperandList(self: *Emitter, ops: []const ast.AsmOperand, depth: usize) Error!void {
        if (ops.len == 0) {
            try self.raw("\n");
            return;
        }
        for (ops, 0..) |op, idx| {
            if (idx == 0) {
                try self.raw(" ");
            } else {
                try self.indent(depth + 1);
                try self.raw("  ");
            }
            try self.emitAsmOperand(op, depth);
            try self.raw(",\n");
        }
    }

    // `[name] "constraint" (body)` — the body is `-> T` for a value-producing
    // output, or an expression for an lvalue output / an input value.
    fn emitAsmOperand(self: *Emitter, op: ast.AsmOperand, depth: usize) Error!void {
        try self.raw("[");
        try self.raw(op.name);
        try self.raw("] ");
        try self.raw(op.constraint);
        try self.raw(" (");
        switch (op.body) {
            .ret_type => |t| {
                try self.raw("-> ");
                try self.emitType(t);
            },
            .expr => |e| try self.emitExprAt(e, depth),
        }
        try self.raw(")");
    }
};

// Whether a slice bound forces zig fmt to space the `..`. zig fmt spaces it
// when a bound is a binary operation or a `catch` — every other expression form
// (ident, literal, call, index, member, deref, unary, grouping) stays tight.
// Flash's `.binary` covers all binary operators (arithmetic, comparison,
// bitwise, shift, `&&`/`||`, `orelse`); `catch` is its own `.catch_expr` node.
fn sliceBoundSpaces(x: ast.Expr) bool {
    return switch (x) {
        .binary, .catch_expr => true,
        else => false,
    };
}

// Map a Flash binary-operator lexeme to its Zig spelling. Only the two logical
// operators differ — Flash's ligature-friendly "&&"/"||" lower to Zig's
// `and`/`or`; every other operator is identical in both languages.
fn zigBinOp(op: []const u8) []const u8 {
    if (std.mem.eql(u8, op, "&&")) return "and";
    if (std.mem.eql(u8, op, "||")) return "or";
    return op;
}

// --- tests ---------------------------------------------------------------

const testing = std.testing;
const Parser = @import("parser.zig").Parser;

fn lowerSrc(arena: std.mem.Allocator, src: []const u8) ![]const u8 {
    var p = Parser.init(arena, src);
    const program = try p.parseProgram();
    return emit(arena, program);
}

test "hello: bind, discard, call lower to diffable Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\export fn main(_ usize, _ argv) noreturn {
        \\    msg := "hello from flash\n"
        \\    _ = flibc.sys.write_fd(1, msg.ptr, msg.len)
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\export fn main(_: usize, _: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    const msg = "hello from flash\n";
        \\    _ = flibc.sys.write_fd(1, msg.ptr, msg.len);
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "clear: cross-import, aliasless void fn, const slice param" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\use console_ui
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\fn sink(bytes []u8) {
        \\    _ = flibc.sys.write_fd(1, bytes.ptr, bytes.len)
        \\}
        \\
        \\export fn main(_ usize, _ argv) noreturn {
        \\    console_ui.screen.clear(sink)
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\const console_ui = @import("console_ui");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\fn sink(bytes: []const u8) void {
        \\    _ = flibc.sys.write_fd(1, bytes.ptr, bytes.len);
        \\}
        \\
        \\export fn main(_: usize, _: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    console_ui.screen.clear(sink);
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "use alias and opt-in mutable slice" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use console_ui as ui
        \\
        \\fn paint(buf []mut u8) {
        \\    ui.fill(buf)
        \\}
    );
    const want =
        \\const ui = @import("console_ui");
        \\
        \\fn paint(buf: []u8) void {
        \\    ui.fill(buf);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "single-item pointer types: *T is const-pointee, *mut T is mutable" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn pass(p *u32, q *mut u32, rows *[4]u8) *mut u32 {
        \\    return q
        \\}
    );
    // `*T` lowers const-pointee (`*const T`), parallel to `[]T` being a const
    // slice; `*mut T` is the mutable opt-in (`*T`). The pointer prefix composes
    // over any element type, including a pointer-to-array (`*[4]u8`).
    const want =
        \\fn pass(p: *const u32, q: *u32, rows: *const [4]u8) *u32 {
        \\    return q;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "function types: `fn(P) R` lowers to `fn (P) R`, and `*` over it is the const pointer" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn install(cb *fn(usize) void, opt ?*fn(i32, u8) bool, raw *mut fn() void) *fn() u8 {
        \\    return undefined
        \\}
    );
    // A `fn(P, …) R` is a function *type*; zig fmt writes the anonymous form
    // with a space after `fn` (unlike a named `fn name(`), and a missing return
    // is `void`. The pointer-ness is the surrounding `*` — `*fn(…)` reuses the
    // const-by-default single-item pointer (`*const fn (…)`), `*mut fn(…)` is the
    // mutable opt-in (`*fn (…)`), and `?` / the parameter types compose as on any
    // other type. Byte-identical to `zig fmt`.
    const want =
        \\fn install(cb: *const fn (usize) void, opt: ?*const fn (i32, u8) bool, raw: *fn () void) *const fn () u8 {
        \\    return undefined;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "function pointer v-table: the explicit-allocator interface shape lowers byte-for-byte" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Allocator = struct {
        \\    ptr *mut anyopaque,
        \\    vtable *VTable,
        \\}
        \\
        \\const VTable = struct {
        \\    alloc *fn(*mut anyopaque, usize) ?[*]mut u8,
        \\    free *fn(*mut anyopaque, []mut u8) void,
        \\}
    );
    // The no-GC keystone: an allocator is a `{ ptr, *VTable }` pair and the
    // v-table is a struct of function pointers — the dynamic-dispatch substrate
    // expressed with no language magic, the shape Zig's `std.mem.Allocator`
    // uses. The `*fn(…) R` fields lower to `*const fn (…) R`; this output is
    // `zig ast-check`-clean and fmt-idempotent.
    const want =
        \\const Allocator = struct {
        \\    ptr: *anyopaque,
        \\    vtable: *const VTable,
        \\};
        \\
        \\const VTable = struct {
        \\    alloc: *const fn (*anyopaque, usize) ?[*]u8,
        \\    free: *const fn (*anyopaque, []u8) void,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "error model: origination, named sets, and infix unions lower byte-for-byte" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const AllocError = error{ OutOfMemory, Overflow }
        \\const One = error{Bad}
        \\
        \\fn dup(path cstr) AllocError!i32 {
        \\    return error.OutOfMemory
        \\}
        \\
        \\fn run() !void {
        \\    return
        \\}
        \\
        \\const VTable = struct {
        \\    alloc *fn(usize) AllocError![]u8,
        \\}
    );
    // The full error model. A named set lowers `error{ A, B }` (zig fmt spaces
    // two-plus members; `error{Bad}` stays tight). `error.Name` is an
    // origination, spelled identically. The infix `E!T` names the set
    // (`AllocError!i32`); the prefix `!T` keeps the inferred set, valid on a
    // function-DECL return (`!void`). The v-table field carries an explicit-set
    // fn-pointer return (`AllocError![]const u8`). Byte-identical to `zig fmt`,
    // `zig ast-check`-clean.
    const want =
        \\const AllocError = error{ OutOfMemory, Overflow };
        \\
        \\const One = error{Bad};
        \\
        \\fn dup(path: [*:0]const u8) AllocError!i32 {
        \\    return error.OutOfMemory;
        \\}
        \\
        \\fn run() !void {
        \\    return;
        \\}
        \\
        \\const VTable = struct {
        \\    alloc: *const fn (usize) AllocError![]const u8,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "sentinel pointer types: [*:0]T composes under *, [N], and ?" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn parse(args *[4]?[*:0]u8, p [*:0]u8) [*:0]u8 {
        \\    return p
        \\}
    );
    // `[*:s]T` is a sentinel-terminated many-item pointer; the terminator expr
    // is kept verbatim. Like `[*]T` the element is const-pointee by default, so
    // the bare `[*:0]u8` lowers to `[*:0]const u8`; the prefix nests under `*` /
    // `[N]` / `?` (here `*const [4]?[*:0]const u8`). This output is byte-identical
    // to `zig fmt` and passes `zig ast-check`.
    const want =
        \\fn parse(args: *const [4]?[*:0]const u8, p: [*:0]const u8) [*:0]const u8 {
        \\    return p;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "volatile many-item pointer: [*]volatile T is const+volatile, [*]mut volatile T writable" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn copy(dst []mut u8, src []u8) {
        \\    const d [*]mut volatile u8 = #ptrCast(dst.ptr)
        \\    d[0] = src[0]
        \\    const s [*]volatile u8 = #ptrCast(src.ptr)
        \\    _ = s[0]
        \\}
    );
    // A volatile pointee is const by default like every pointer family: `[*]volatile T`
    // is const+volatile (an MMIO read or a read-only mapping), and `[*]mut volatile T`
    // is the writable form (the byte-copy destination here). The `volatile` qualifier
    // sits after the implicit `const`, both spaced from the element type as zig fmt lays
    // it out. Writing through the `mut` form and only reading through the const form keeps
    // the output `zig ast-check`-clean; it is byte-identical to `zig fmt`.
    const want =
        \\fn copy(dst: []u8, src: []const u8) void {
        \\    const d: [*]volatile u8 = @ptrCast(dst.ptr);
        \\    d[0] = src[0];
        \\    const s: [*]const volatile u8 = @ptrCast(src.ptr);
        \\    _ = s[0];
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "volatile single-item pointer: *volatile T is const+volatile, *mut volatile T writable" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn mmio(reg *mut volatile u32, status *volatile u32) u32 {
        \\    reg.* = 1
        \\    return status.*
        \\}
    );
    // The single-item volatile forms mirror the many-item ones: `*volatile T` is
    // const+volatile (a read-only register), `*mut volatile T` writable (the
    // configuration register written here). This is the canonical MMIO shape —
    // a store through the writable register, a load through the read-only one.
    // The `const` is implicit before `volatile`. Byte-identical to `zig fmt`,
    // `zig ast-check`-clean (the store needs the mutable pointee).
    const want =
        \\fn mmio(reg: *volatile u32, status: *const volatile u32) u32 {
        \\    reg.* = 1;
        \\    return status.*;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "pointer dereference: p.* reads and stores through a single-item pointer" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn store(p *mut u32, v u32) {
        \\    p.* = v
        \\    _ = p.*
        \\}
    );
    // `p.*` is a single-item pointer dereference; it lowers verbatim and is a
    // valid lvalue, so `p.* = v` stores through the pointer while `_ = p.*`
    // reads it. The mutable pointee (`*mut u32` -> `*u32`) is what the store
    // needs. This output is byte-identical to `zig fmt` and passes `zig
    // ast-check` (both params used).
    const want =
        \\fn store(p: *u32, v: u32) void {
        \\    p.* = v;
        \\    _ = p.*;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "sentinel slice: a[lo..hi :s] lowers byte-for-byte with zig fmt's space before the colon" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn span(buf []mut u8, pos usize, n usize) [*:0]mut u8 {
        \\    return buf[pos..n :0].ptr
        \\}
    );
    // `a[lo..hi :s]` is a sentinel-terminated slice; zig fmt puts a space before
    // the `:` and none after, with no spaces around `..`. The `.ptr` postfix is
    // the real use — a nul-terminated pointer out of a buffer. This output is
    // byte-identical to `zig fmt` and passes `zig ast-check`.
    const want =
        \\fn span(buf: []u8, pos: usize, n: usize) [*:0]u8 {
        \\    return buf[pos..n :0].ptr;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "sentinel array: [N:s]T lowers verbatim, no spaces around the colon" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn first(buf [4:0]u8) u8 {
        \\    return buf[0]
        \\}
    );
    // `[N:s]T` is a fixed-length array with a trailing sentinel — `[4:0]u8` is
    // four bytes plus a guaranteed `0` at index 4. Length and sentinel render
    // verbatim with no spaces around the `:`, exactly as zig fmt lays it out
    // (unlike the sentinel *slice* `[lo..hi :s]`, which is spaced before the `:`).
    // This output is byte-identical to `zig fmt` and passes `zig ast-check`.
    const want =
        \\fn first(buf: [4:0]u8) u8 {
        \\    return buf[0];
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "inferred sentinel array: [_:s]T{ … } lowers as the argv-style null-terminated vector" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn run() {
        \\    const argv = [_:null]?[*:0]u8{ "sh" }
        \\    _ = argv
        \\}
    );
    // `[_:s]T` is the inferred-length sentinel array — its length comes from the
    // initializer, the sentinel from the `:`. The canonical use is the
    // null-terminated argument vector an exec call wants: `[_:null]?[*:0]u8`
    // lowers the const-default many-item element to `?[*:0]const u8`. A single
    // positional element renders unspaced (`{"sh"}`), as zig fmt does. This
    // output is byte-identical to `zig fmt` and passes `zig ast-check`.
    const want =
        \\fn run() void {
        \\    const argv = [_:null]?[*:0]const u8{"sh"};
        \\    _ = argv;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "generic type application: Name(args) lowers verbatim in type position" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn build(a List(u8), b Map(Key, Val)) List(Ring(8)) {
        \\    _ = a
        \\    _ = b
        \\    return undefined
        \\}
    );
    // A generic instance named in type position — a parameter (`List(u8)`,
    // multi-arg `Map(Key, Val)`), a return (nested `List(Ring(8))` with a value
    // argument `8`) — lowers as the call zig reads it; the arguments are parsed
    // exactly as a value-position call's. This output is byte-identical to `zig
    // fmt`. A declared generic (`Box(u8)` over `fn Box(comptime T type) type`)
    // is `zig ast-check`-clean through the full pipeline.
    const want =
        \\fn build(a: List(u8), b: Map(Key, Val)) List(Ring(8)) {
        \\    _ = a;
        \\    _ = b;
        \\    return undefined;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "slice bound spacing: zig fmt spaces `..` only when a bound is a binary op" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // zig fmt renders `a[lo..hi]` tight when both bounds are simple (idents,
    // literals, member access, indexing), and spaces the `..` to `a[lo .. hi]`
    // when either bound is a binary operation. An open-ended `a[lo + 1 ..]` keeps
    // the space only before `..`. The sentinel `:s` form follows the same rule.
    // This output is byte-identical to `zig fmt` and passes `zig ast-check`.
    const got = try lowerSrc(a.allocator(),
        \\fn slices(buf []mut u8, lo usize, hi usize) {
        \\    _ = buf[lo..hi]
        \\    _ = buf[lo .. hi + 1]
        \\    _ = buf[lo + 1 .. hi]
        \\    _ = buf[lo + 1 ..]
        \\    _ = buf[lo..][0..hi]
        \\    _ = buf[lo .. hi + 1 :0]
        \\}
    );
    const want =
        \\fn slices(buf: []u8, lo: usize, hi: usize) void {
        \\    _ = buf[lo..hi];
        \\    _ = buf[lo .. hi + 1];
        \\    _ = buf[lo + 1 .. hi];
        \\    _ = buf[lo + 1 ..];
        \\    _ = buf[lo..][0..hi];
        \\    _ = buf[lo .. hi + 1 :0];
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "meminfo: an anonymous struct literal in a printf call" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\export fn main(_ usize, _ argv) noreturn {
        \\    flibc.printf("free pages: %u\n", .{flibc.sys.dump_free()})
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\export fn main(_: usize, _: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    flibc.printf("free pages: %u\n", .{flibc.sys.dump_free()});
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "expressions: precedence, index/slice, unary, builtins, && -> and, grouping" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, n usize, m usize) {
        \\    _ = n + 1 < m
        \\    _ = xs[n] != 0
        \\    _ = xs[0..n]
        \\    _ = -xs[n] + #intCast(n % 10)
        \\    _ = &xs
        \\    _ = n < m && m != 0
        \\    _ = n * (m + n)
        \\}
    );
    const want =
        \\fn f(xs: []const u8, n: usize, m: usize) void {
        \\    _ = n + 1 < m;
        \\    _ = xs[n] != 0;
        \\    _ = xs[0..n];
        \\    _ = -xs[n] + @intCast(n % 10);
        \\    _ = &xs;
        \\    _ = n < m and m != 0;
        \\    _ = n * (m + n);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "control-flow shapes: while, for-in, if/else-if/else, compound assign, return" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, n usize) usize {
        \\    var total usize = 0
        \\    for x in xs {
        \\        _ = x
        \\        total += 1
        \\    }
        \\    while n > 0 {
        \\        n -= 1
        \\    }
        \\    if n == 0 {
        \\        return total
        \\    } else if n == 1 {
        \\        return n
        \\    } else {
        \\        return 0
        \\    }
        \\}
    );
    const want =
        \\fn f(xs: []const u8, n: usize) usize {
        \\    var total: usize = 0;
        \\    for (xs) |x| {
        \\        _ = x;
        \\        total += 1;
        \\    }
        \\    while (n > 0) {
        \\        n -= 1;
        \\    }
        \\    if (n == 0) {
        \\        return total;
        \\    } else if (n == 1) {
        \\        return n;
        \\    } else {
        \\        return 0;
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "while optional-capture lowers to `while (expr) |x| { … }`" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn drain(it Iter) {
        \\    while it.next() |x| {
        \\        _ = x
        \\    }
        \\}
    );
    // The `| ident | {` capture shape stops the condition parse (the
    // atCapturePipe guard); the capture renders after the parenthesised
    // condition, exactly as the `if` capture does. Byte-identical to zig fmt.
    const want =
        \\fn drain(it: Iter) void {
        \\    while (it.next()) |x| {
        \\        _ = x;
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "for surface: range iterator and indexed capture lower to Zig's `for (lo..hi)` / `for (xs, 0..)`" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, n usize) {
        \\    for i in 0..n {
        \\        _ = i
        \\    }
        \\    for i in lo..hi {
        \\        _ = i
        \\    }
        \\    for x, i in xs {
        \\        _ = x
        \\        _ = i
        \\    }
        \\}
    );
    // `for i in lo..hi` → `for (lo..hi) |i|`: the `..` range is emitted with no
    // surrounding spaces (as zig fmt keeps a range). A second capture appends the
    // implicit index range `0..`, giving `for (xs, 0..) |x, i|`. Both are
    // byte-identical to zig fmt and pass zig ast-check.
    const want =
        \\fn f(xs: []const u8, n: usize) void {
        \\    for (0..n) |i| {
        \\        _ = i;
        \\    }
        \\    for (lo..hi) |i| {
        \\        _ = i;
        \\    }
        \\    for (xs, 0..) |x, i| {
        \\        _ = x;
        \\        _ = i;
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "inline for lowers to Zig's `inline for` across element, range, indexed, and else shapes" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, n usize) {
        \\    inline for x in xs {
        \\        _ = x
        \\    }
        \\    inline for i in 0..n {
        \\        _ = i
        \\    }
        \\    inline for x, i in xs {
        \\        _ = x
        \\        _ = i
        \\    } else {
        \\        g()
        \\    }
        \\}
    );
    // The `inline ` prefix rides every for shape unchanged — element, range,
    // indexed capture, and the loop else arm. Byte-identical to zig fmt.
    const want =
        \\fn f(xs: []const u8, n: usize) void {
        \\    inline for (xs) |x| {
        \\        _ = x;
        \\    }
        \\    inline for (0..n) |i| {
        \\        _ = i;
        \\    }
        \\    inline for (xs, 0..) |x, i| {
        \\        _ = x;
        \\        _ = i;
        \\    } else {
        \\        g();
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "for surface: a `_` capture lowers verbatim to Zig's `|_|` discard" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, n usize) {
        \\    for _ in 0..n {
        \\        tick()
        \\    }
        \\    for x, _ in xs {
        \\        _ = x
        \\    }
        \\}
    );
    // The element and index discards lower to Zig's `|_|` and `|x, _|` — the
    // verbatim capture spelling, byte-identical to zig fmt.
    const want =
        \\fn f(xs: []const u8, n: usize) void {
        \\    for (0..n) |_| {
        \\        tick();
        \\    }
        \\    for (xs, 0..) |x, _| {
        \\        _ = x;
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "if-expression lowers to a parenthesised-condition value if" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The expression form of `if` (distinct from the statement form): both arms
    // are expressions and `else` is mandatory. The condition is parenthesised in
    // Flash too (required for a value `if`); the parens are not doubled in the
    // lowered Zig, and the arms render inline.
    const got = try lowerSrc(a.allocator(),
        \\fn classify(n usize) usize {
        \\    x := if (n == 0) 1 else 2
        \\    return if (n > x) n else x
        \\}
    );
    const want =
        \\fn classify(n: usize) usize {
        \\    const x = if (n == 0) 1 else 2;
        \\    return if (n > x) n else x;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "typed struct/union literal lowers, and a header brace stays the body" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // `Type{ … }` lowers with the type prefix and the same brace-spacing as the
    // anonymous form. Crucially, the `{` after a control-flow header subject
    // (`while i < buf.len {`) still opens the body — it is not swallowed as a
    // typed literal — so the loop and the initializer coexist.
    const got = try lowerSrc(a.allocator(),
        \\const Action = union(enum) {
        \\    none,
        \\    echo u8,
        \\}
        \\
        \\fn step(buf []u8, byte u8) Action {
        \\    var i usize = 0
        \\    while i < buf.len {
        \\        i += 1
        \\    }
        \\    return Action{ .echo = byte }
        \\}
    );
    const want =
        \\const Action = union(enum) {
        \\    none,
        \\    echo: u8,
        \\};
        \\
        \\fn step(buf: []const u8, byte: u8) Action {
        \\    var i: usize = 0;
        \\    while (i < buf.len) {
        \\        i += 1;
        \\    }
        \\    return Action{ .echo = byte };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "switch expression lowers prongs, value lists, ranges, and block arms" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // Covers every prong shape: a value list (`' ', '\t'`), an inclusive range
    // (`'0'...'9'`, no spaces around `...`), an inline `if`-expression arm, a
    // multi-statement `label: { … }` arm with valued breaks, and the `else`
    // default. The subject is parenthesised; prongs sit one per line at depth + 1.
    const got = try lowerSrc(a.allocator(),
        \\fn classify(c u8) u8 {
        \\    return switch c {
        \\        ' ', '\t' => 0,
        \\        '0'...'9' => 1,
        \\        '+' => if (c == 0) 2 else 3,
        \\        else => blk: {
        \\            if c == 0 {
        \\                break :blk 9
        \\            }
        \\            break :blk 4
        \\        },
        \\    }
        \\}
    );
    const want =
        \\fn classify(c: u8) u8 {
        \\    return switch (c) {
        \\        ' ', '\t' => 0,
        \\        '0'...'9' => 1,
        \\        '+' => if (c == 0) 2 else 3,
        \\        else => blk: {
        \\            if (c == 0) {
        \\                break :blk 9;
        \\            }
        \\            break :blk 4;
        \\        },
        \\    };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "switch prong payload captures and a void block arm lower like Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The fsh `dispatch` shape: a `=> |x|` payload capture on three prongs
    // (`.err => |e|`, `.single => |n|`, `.piped => |p|`), a void `.empty => {}`
    // arm (an unlabelled empty block), and a nested `switch e { … }` as a prong
    // body. The capture renders `|x| ` after `=>`; the void arm collapses to `{}`;
    // the bare-statement switch takes no trailing `;`.
    const got = try lowerSrc(a.allocator(),
        \\fn dispatch(line []u8) {
        \\    var argv [tok.MAX_ARGS]?[*:0]mut u8 = undefined
        \\    var buf [TOK_BUF]u8 = undefined
        \\    switch tok.tokenize(line, &argv, &buf) {
        \\        .empty => {},
        \\        .err => |e| switch e {
        \\            .too_many_pipes => emit(2, "fsh: only one pipe supported\n"),
        \\            .empty_side => emit(2, "fsh: missing command around |\n"),
        \\        },
        \\        .single => |n| runSingle(&argv, n),
        \\        .piped => |p| runPiped(&argv, p),
        \\    }
        \\}
    );
    const want =
        \\fn dispatch(line: []const u8) void {
        \\    var argv: [tok.MAX_ARGS]?[*:0]u8 = undefined;
        \\    var buf: [TOK_BUF]u8 = undefined;
        \\    switch (tok.tokenize(line, &argv, &buf)) {
        \\        .empty => {},
        \\        .err => |e| switch (e) {
        \\            .too_many_pipes => emit(2, "fsh: only one pipe supported\n"),
        \\            .empty_side => emit(2, "fsh: missing command around |\n"),
        \\        },
        \\        .single => |n| runSingle(&argv, n),
        \\        .piped => |p| runPiped(&argv, p),
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "align(N) binding qualifier and a bare-return switch prong lower like Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The fsh `repl` core: an `align(16)` qualifier on a typed binding (between
    // the type and `=`), and a `.eof => return` prong whose bare `return` stops
    // at the prong comma instead of consuming it as a value.
    const got = try lowerSrc(a.allocator(),
        \\fn repl() {
        \\    const comp Completion align(16) = .{ .a = 1 }
        \\    switch read(comp) {
        \\        .eof => return,
        \\        .line => |l| {
        \\            handle(l)
        \\        },
        \\    }
        \\}
    );
    const want =
        \\fn repl() void {
        \\    const comp: Completion align(16) = .{ .a = 1 };
        \\    switch (read(comp)) {
        \\        .eof => return,
        \\        .line => |l| {
        \\            handle(l);
        \\        },
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "labeled block expression and valued break lower with correct depth" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // `label: { … }` is an expression whose value is a `break :label v` inside
    // it. The block body indents at depth + 1 and closes at depth even when
    // nested under `return`; the labelled breaks render `break :blk v`.
    const got = try lowerSrc(a.allocator(),
        \\fn pick(n usize) usize {
        \\    return blk: {
        \\        if n == 0 {
        \\            break :blk 1
        \\        }
        \\        break :blk 2
        \\    }
        \\}
    );
    const want =
        \\fn pick(n: usize) usize {
        \\    return blk: {
        \\        if (n == 0) {
        \\            break :blk 1;
        \\        }
        \\        break :blk 2;
        \\    };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "empty blocks collapse to `{}` across fn, while, for, and if/else" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn noop() {
        \\}
        \\
        \\fn loops(n usize) {
        \\    var i usize = 0
        \\    while i < n {
        \\    }
        \\    for x in n {
        \\    }
        \\}
        \\
        \\fn branch(c bool) {
        \\    if c {
        \\    } else {
        \\    }
        \\}
    );
    // An empty statement list collapses to a one-line `{}` (matching zig fmt) at
    // every block site — function body, `while`, `for`, and both `if` arms.
    // Byte-identical to `zig fmt`; the collapsed `fn noop() void {}` is also
    // `ast-check`-clean.
    const want =
        \\fn noop() void {}
        \\
        \\fn loops(n: usize) void {
        \\    var i: usize = 0;
        \\    while (i < n) {}
        \\    for (n) |x| {}
        \\}
        \\
        \\fn branch(c: bool) void {
        \\    if (c) {} else {}
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "error-union surface: !T/?T, try, catch, defer, errdefer, optional-capture if" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn open(p cstr) !i32 {
        \\    fd := try sys.open(p)
        \\    errdefer _ = sys.close(fd)
        \\    defer _ = sys.flush()
        \\    return fd
        \\}
        \\
        \\fn pick(xs []u8) ?u8 {
        \\    if find(xs) |hit| {
        \\        return hit
        \\    }
        \\    _ = run(xs) catch |e| report(e)
        \\    return none
        \\}
    );
    const want =
        \\fn open(p: [*:0]const u8) !i32 {
        \\    const fd = try sys.open(p);
        \\    errdefer _ = sys.close(fd);
        \\    defer _ = sys.flush();
        \\    return fd;
        \\}
        \\
        \\fn pick(xs: []const u8) ?u8 {
        \\    if (find(xs)) |hit| {
        \\        return hit;
        \\    }
        \\    _ = run(xs) catch |e| report(e);
        \\    return none;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "catch block recovery: bare void block, captured block, value position" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn run() !u8 {
        \\    flush() catch {}
        \\    _ = work() catch |e| {
        \\        log(e)
        \\        return 0
        \\    }
        \\    x := work() catch { return 0 }
        \\    return x
        \\}
    );
    const want =
        \\fn run() !u8 {
        \\    flush() catch {};
        \\    _ = work() catch |e| {
        \\        log(e);
        \\        return 0;
        \\    };
        \\    const x = work() catch {
        \\        return 0;
        \\    };
        \\    return x;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "dmesg port: array decl + if guard lower to diffable Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\export fn main(_ usize, _ argv) noreturn {
        \\    var buf [flibc.KLOG_SIZE]u8 = undefined
        \\    n := flibc.sys.klog_read(&buf, buf.len)
        \\    if n > 0 {
        \\        _ = flibc.sys.write_fd(1, &buf, #intCast(n))
        \\    }
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\export fn main(_: usize, _: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    var buf: [flibc.KLOG_SIZE]u8 = undefined;
        \\    const n = flibc.sys.klog_read(&buf, buf.len);
        \\    if (n > 0) {
        \\        _ = flibc.sys.write_fd(1, &buf, @intCast(n));
        \\    }
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "echo port: while + orelse break + nul-scan helper" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\fn emit(s []u8) {
        \\    _ = flibc.sys.write_fd(1, s.ptr, s.len)
        \\}
        \\
        \\fn emitz(s cstr) {
        \\    var n usize = 0
        \\    while s[n] != 0 {
        \\        n += 1
        \\    }
        \\    _ = flibc.sys.write_fd(1, s, n)
        \\}
        \\
        \\export fn main(argc usize, argv argv) noreturn {
        \\    var i usize = 1
        \\    while i < argc {
        \\        s := argv[i] orelse break
        \\        emitz(s)
        \\        if i + 1 < argc {
        \\            emit(" ")
        \\        }
        \\        i += 1
        \\    }
        \\    emit("\n")
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\fn emit(s: []const u8) void {
        \\    _ = flibc.sys.write_fd(1, s.ptr, s.len);
        \\}
        \\
        \\fn emitz(s: [*:0]const u8) void {
        \\    var n: usize = 0;
        \\    while (s[n] != 0) {
        \\        n += 1;
        \\    }
        \\    _ = flibc.sys.write_fd(1, s, n);
        \\}
        \\
        \\export fn main(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    var i: usize = 1;
        \\    while (i < argc) {
        \\        const s = argv[i] orelse break;
        \\        emitz(s);
        \\        if (i + 1 < argc) {
        \\            emit(" ");
        \\        }
        \\        i += 1;
        \\    }
        \\    emit("\n");
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "cat port: top-const, if/else, nested while, continue, import alias" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\use syscall_defs as defs
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\const BUF_LEN usize = 512
        \\
        \\fn drain(fd i32) {
        \\    var buf [BUF_LEN]u8 = undefined
        \\    while true {
        \\        n := flibc.sys.read(fd, &buf, buf.len)
        \\        if n <= 0 {
        \\            break
        \\        }
        \\        _ = flibc.sys.write_fd(1, &buf, #intCast(n))
        \\    }
        \\}
        \\
        \\export fn main(argc usize, argv argv) noreturn {
        \\    if argc <= 1 {
        \\        drain(0)
        \\    } else {
        \\        var i usize = 1
        \\        while i < argc {
        \\            path := argv[i] orelse break
        \\            i += 1
        \\            fd := flibc.sys.open(path)
        \\            if fd < 0 {
        \\                var msg []u8 = "cat: cannot open\n"
        \\                if fd == -defs.EACCES {
        \\                    msg = "cat: Permission denied\n"
        \\                }
        \\                _ = flibc.sys.write_fd(2, msg.ptr, msg.len)
        \\                continue
        \\            }
        \\            drain(fd)
        \\            _ = flibc.sys.close(fd)
        \\        }
        \\    }
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\const defs = @import("syscall_defs");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\const BUF_LEN: usize = 512;
        \\
        \\fn drain(fd: i32) void {
        \\    var buf: [BUF_LEN]u8 = undefined;
        \\    while (true) {
        \\        const n = flibc.sys.read(fd, &buf, buf.len);
        \\        if (n <= 0) {
        \\            break;
        \\        }
        \\        _ = flibc.sys.write_fd(1, &buf, @intCast(n));
        \\    }
        \\}
        \\
        \\export fn main(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    if (argc <= 1) {
        \\        drain(0);
        \\    } else {
        \\        var i: usize = 1;
        \\        while (i < argc) {
        \\            const path = argv[i] orelse break;
        \\            i += 1;
        \\            const fd = flibc.sys.open(path);
        \\            if (fd < 0) {
        \\                var msg: []const u8 = "cat: cannot open\n";
        \\                if (fd == -defs.EACCES) {
        \\                    msg = "cat: Permission denied\n";
        \\                }
        \\                _ = flibc.sys.write_fd(2, msg.ptr, msg.len);
        \\                continue;
        \\            }
        \\            drain(fd);
        \\            _ = flibc.sys.close(fd);
        \\        }
        \\    }
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "ls port: struct literal, member address-of, && condition" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\fn emit(s []u8) {
        \\    _ = flibc.sys.write_fd(1, s.ptr, s.len)
        \\}
        \\
        \\fn listDir(path cstr) {
        \\    var d flibc.Dirent = .{}
        \\    var i u64 = 0
        \\    while flibc.sys.readdir(path, i, &d) == 0 {
        \\        var n usize = 0
        \\        while n < d.name.len && d.name[n] != 0 {
        \\            n += 1
        \\        }
        \\        _ = flibc.sys.write_fd(1, &d.name, n)
        \\        if d.d_type == flibc.DT_DIR {
        \\            emit("/")
        \\        }
        \\        emit("\n")
        \\        i += 1
        \\    }
        \\}
        \\
        \\export fn main(argc usize, argv argv) noreturn {
        \\    if argc <= 1 {
        \\        listDir(".")
        \\    } else {
        \\        var a usize = 1
        \\        while a < argc {
        \\            path := argv[a] orelse break
        \\            listDir(path)
        \\            a += 1
        \\        }
        \\    }
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\fn emit(s: []const u8) void {
        \\    _ = flibc.sys.write_fd(1, s.ptr, s.len);
        \\}
        \\
        \\fn listDir(path: [*:0]const u8) void {
        \\    var d: flibc.Dirent = .{};
        \\    var i: u64 = 0;
        \\    while (flibc.sys.readdir(path, i, &d) == 0) {
        \\        var n: usize = 0;
        \\        while (n < d.name.len and d.name[n] != 0) {
        \\            n += 1;
        \\        }
        \\        _ = flibc.sys.write_fd(1, &d.name, n);
        \\        if (d.d_type == flibc.DT_DIR) {
        \\            emit("/");
        \\        }
        \\        emit("\n");
        \\        i += 1;
        \\    }
        \\}
        \\
        \\export fn main(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    if (argc <= 1) {
        \\        listDir(".");
        \\    } else {
        \\        var a: usize = 1;
        \\        while (a < argc) {
        \\            const path = argv[a] orelse break;
        \\            listDir(path);
        \\            a += 1;
        \\        }
        \\    }
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "readfile port: error-union fns, try, defer, errdefer, catch capture" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\const BUF_LEN usize = 512
        \\
        \\fn dup(path cstr) !i32 {
        \\    fd := try flibc.sys.open(path)
        \\    errdefer _ = flibc.sys.close(fd)
        \\    _ = try flibc.sys.fstat(fd)
        \\    return fd
        \\}
        \\
        \\fn copy(path cstr) !usize {
        \\    fd := try dup(path)
        \\    defer _ = flibc.sys.close(fd)
        \\    var buf [BUF_LEN]u8 = undefined
        \\    var total usize = 0
        \\    while true {
        \\        n := try flibc.sys.read(fd, &buf, buf.len)
        \\        if n == 0 {
        \\            break
        \\        }
        \\        _ = flibc.sys.write_fd(1, &buf, #intCast(n))
        \\        total += n
        \\    }
        \\    return total
        \\}
        \\
        \\fn report(e flibc.Error) usize {
        \\    _ = e
        \\    _ = flibc.sys.write_fd(2, "readfile: I/O error\n", 20)
        \\    return 0
        \\}
        \\
        \\export fn main(argc usize, argv argv) noreturn {
        \\    var i usize = 1
        \\    while i < argc {
        \\        path := argv[i] orelse break
        \\        _ = copy(path) catch |e| report(e)
        \\        i += 1
        \\    }
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\const BUF_LEN: usize = 512;
        \\
        \\fn dup(path: [*:0]const u8) !i32 {
        \\    const fd = try flibc.sys.open(path);
        \\    errdefer _ = flibc.sys.close(fd);
        \\    _ = try flibc.sys.fstat(fd);
        \\    return fd;
        \\}
        \\
        \\fn copy(path: [*:0]const u8) !usize {
        \\    const fd = try dup(path);
        \\    defer _ = flibc.sys.close(fd);
        \\    var buf: [BUF_LEN]u8 = undefined;
        \\    var total: usize = 0;
        \\    while (true) {
        \\        const n = try flibc.sys.read(fd, &buf, buf.len);
        \\        if (n == 0) {
        \\            break;
        \\        }
        \\        _ = flibc.sys.write_fd(1, &buf, @intCast(n));
        \\        total += n;
        \\    }
        \\    return total;
        \\}
        \\
        \\fn report(e: flibc.Error) usize {
        \\    _ = e;
        \\    _ = flibc.sys.write_fd(2, "readfile: I/O error\n", 20);
        \\    return 0;
        \\}
        \\
        \\export fn main(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    var i: usize = 1;
        \\    while (i < argc) {
        \\        const path = argv[i] orelse break;
        \\        _ = copy(path) catch |e| report(e);
        \\        i += 1;
        \\    }
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "struct/enum definitions and literals lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Point = struct {
        \\    x i32,
        \\    y i32,
        \\}
        \\
        \\const Kind = enum(u8) {
        \\    file,
        \\    dir,
        \\}
        \\
        \\const Mode = enum {
        \\    on,
        \\    off,
        \\}
        \\
        \\fn make(n i32) Point {
        \\    return .{ .x = n, .y = 0 }
        \\}
        \\
        \\fn demo(n i32) {
        \\    _ = .{n}
        \\    _ = .{ n, n }
        \\    var k Kind = .file
        \\    _ = k
        \\}
    );
    const want =
        \\const Point = struct {
        \\    x: i32,
        \\    y: i32,
        \\};
        \\
        \\const Kind = enum(u8) {
        \\    file,
        \\    dir,
        \\};
        \\
        \\const Mode = enum {
        \\    on,
        \\    off,
        \\};
        \\
        \\fn make(n: i32) Point {
        \\    return .{ .x = n, .y = 0 };
        \\}
        \\
        \\fn demo(n: i32) void {
        \\    _ = .{n};
        \\    _ = .{ n, n };
        \\    var k: Kind = .file;
        \\    _ = k;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "union(enum) definitions lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A bare void variant stays bare; a typed variant gains the `: T`; the slice
    // payload `[]u8` lowers through emitType to `[]const u8`, like any other.
    const got = try lowerSrc(a.allocator(),
        \\const Result = union(enum) {
        \\    empty,
        \\    single usize,
        \\    line []u8,
        \\    piped Piped,
        \\}
    );
    const want =
        \\const Result = union(enum) {
        \\    empty,
        \\    single: usize,
        \\    line: []const u8,
        \\    piped: Piped,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "empty container definitions lower to zig fmt's one-line `{}` form" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Empty = struct {}
        \\const E = enum {}
        \\const T = enum(u8) {}
        \\const U = union {}
        \\const V = union(enum) {}
    );
    const want =
        \\const Empty = struct {};
        \\
        \\const E = enum {};
        \\
        \\const T = enum(u8) {};
        \\
        \\const U = union {};
        \\
        \\const V = union(enum) {};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "pub visibility lowers to a pub prefix" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // `pub` precedes `export`; a `pub export fn` keeps the C calling convention.
    // A bare (non-pub) const/fn is unchanged.
    const got = try lowerSrc(a.allocator(),
        \\pub const MAX usize = 16
        \\const MIN usize = 0
        \\pub fn span(n usize) usize {
        \\    return n + MIN
        \\}
        \\pub export fn main() {
        \\    _ = span(MAX)
        \\}
    );
    const want =
        \\pub const MAX: usize = 16;
        \\
        \\const MIN: usize = 0;
        \\
        \\pub fn span(n: usize) usize {
        \\    return n + MIN;
        \\}
        \\
        \\pub export fn main() callconv(.c) void {
        \\    _ = span(MAX);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "inline fn lowers to an inline prefix" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // `inline` follows `pub` and takes no callconv (that is export-only); it
    // applies to a top-level function and to a struct method alike. A bare fn is
    // unchanged.
    const got = try lowerSrc(a.allocator(),
        \\inline fn hot(n usize) usize {
        \\    return n + 1
        \\}
        \\
        \\const Math = struct {
        \\    inline fn twice(n usize) usize {
        \\        return n + n
        \\    }
        \\}
        \\
        \\pub inline fn exposed(n usize) usize {
        \\    return hot(n)
        \\}
    );
    const want =
        \\inline fn hot(n: usize) usize {
        \\    return n + 1;
        \\}
        \\
        \\const Math = struct {
        \\    inline fn twice(n: usize) usize {
        \\        return n + n;
        \\    }
        \\};
        \\
        \\pub inline fn exposed(n: usize) usize {
        \\    return hot(n);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "enum discriminants lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Errno = enum(u8) {
        \\    ok = 0,
        \\    perm = 1,
        \\    again,
        \\}
        \\
        \\const Mode = enum(u32) {
        \\    none = 0,
        \\    exec = 0x10,
        \\}
    );
    // Each explicit discriminant emits as ` = <expr>`, the literal kept verbatim
    // (decimal and `0x` hex alike); an implicit variant stays a bare name, and
    // mixing the two in one enum is preserved.
    const want =
        \\const Errno = enum(u8) {
        \\    ok = 0,
        \\    perm = 1,
        \\    again,
        \\};
        \\
        \\const Mode = enum(u32) {
        \\    none = 0,
        \\    exec = 0x10,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "bitwise and shift expressions lower verbatim" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(a u32, b u32) {
        \\    _ = a << 2
        \\    _ = a >> 1
        \\    _ = a & b
        \\    _ = a | b
        \\    _ = a ^ b
        \\}
    );
    // Every bitwise/shift operator is spelled identically in Zig, so each
    // passes through unchanged (only "&&"/"||" are remapped, to and/or).
    const want =
        \\fn f(a: u32, b: u32) void {
        \\    _ = a << 2;
        \\    _ = a >> 1;
        \\    _ = a & b;
        \\    _ = a | b;
        \\    _ = a ^ b;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "bitwise and shift compound assignment lowers verbatim" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(flags u32) {
        \\    var f2 u32 = flags
        \\    f2 &= 0x0F
        \\    f2 |= 0x10
        \\    f2 ^= 0xFF
        \\    f2 <<= 1
        \\    f2 >>= 2
        \\}
    );
    // Zig spells these compound assignments identically, so the op lexeme passes
    // through unchanged (the "&&"/"||" remap is expression-only and does not
    // touch the assignment operators).
    const want =
        \\fn f(flags: u32) void {
        \\    var f2: u32 = flags;
        \\    f2 &= 0x0F;
        \\    f2 |= 0x10;
        \\    f2 ^= 0xFF;
        \\    f2 <<= 1;
        \\    f2 >>= 2;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "bitflag enum discriminants lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The motivating case for shifts: a flag enum whose discriminants are
    // `1 << n`. Now that the shift operators lex and parse, the discriminant
    // expression lowers verbatim and stays byte-exact, valid Zig.
    const got = try lowerSrc(a.allocator(),
        \\const Flags = enum(u8) {
        \\    none = 0,
        \\    read = 1 << 0,
        \\    write = 1 << 1,
        \\    exec = 1 << 2,
        \\}
    );
    const want =
        \\const Flags = enum(u8) {
        \\    none = 0,
        \\    read = 1 << 0,
        \\    write = 1 << 1,
        \\    exec = 1 << 2,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "struct methods and an associated constant lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Point = struct {
        \\    x i32,
        \\    y i32,
        \\
        \\    fn sum(self Point) i32 {
        \\        return self.x + self.y
        \\    }
        \\
        \\    const ZERO i32 = 0
        \\}
    );
    const want =
        \\const Point = struct {
        \\    x: i32,
        \\    y: i32,
        \\
        \\    fn sum(self: Point) i32 {
        \\        return self.x + self.y;
        \\    }
        \\
        \\    const ZERO: i32 = 0;
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "a declaration-only struct lowers without a leading blank line" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // No fields: the first method follows '{' directly (zig fmt strips a blank
    // line there), and consecutive methods stay one blank line apart.
    const got = try lowerSrc(a.allocator(),
        \\const Counter = struct {
        \\    fn zero() usize {
        \\        return 0
        \\    }
        \\
        \\    fn bump(n usize) usize {
        \\        return n + 1
        \\    }
        \\}
    );
    const want =
        \\const Counter = struct {
        \\    fn zero() usize {
        \\        return 0;
        \\    }
        \\
        \\    fn bump(n: usize) usize {
        \\        return n + 1;
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "enum methods and an associated constant lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Color = enum(u8) {
        \\    red,
        \\    green = 5,
        \\
        \\    const COUNT usize = 2
        \\
        \\    fn isRed(self Color) bool {
        \\        return self == .red
        \\    }
        \\}
    );
    const want =
        \\const Color = enum(u8) {
        \\    red,
        \\    green = 5,
        \\
        \\    const COUNT: usize = 2;
        \\
        \\    fn isRed(self: Color) bool {
        \\        return self == .red;
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "union methods lower to canonical Zig" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A tagged union with a method; a declaration-only enum gets no leading
    // blank line, matching the struct rule.
    const got = try lowerSrc(a.allocator(),
        \\const Tok = union(enum) {
        \\    eof,
        \\    int usize,
        \\
        \\    fn isEof(self Tok) bool {
        \\        return self == .eof
        \\    }
        \\}
        \\
        \\const Util = enum {
        \\    fn zero() usize {
        \\        return 0
        \\    }
        \\}
    );
    const want =
        \\const Tok = union(enum) {
        \\    eof,
        \\    int: usize,
        \\
        \\    fn isEof(self: Tok) bool {
        \\        return self == .eof;
        \\    }
        \\};
        \\
        \\const Util = enum {
        \\    fn zero() usize {
        \\        return 0;
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "struct field default values lower after the type" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A default value renders as `name: T = expr,` — a literal, `undefined`, or
    // an empty `.{}` alike; a field with no default keeps the bare `name: T,`.
    const got = try lowerSrc(a.allocator(),
        \\const Slot = struct {
        \\    bytes [CAP]u8 = undefined,
        \\    seen [2]u8 = .{},
        \\    len usize = 0,
        \\    src []mut u8,
        \\}
    );
    const want =
        \\const Slot = struct {
        \\    bytes: [CAP]u8 = undefined,
        \\    seen: [2]u8 = .{},
        \\    len: usize = 0,
        \\    src: []u8,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "multiline string as a top-level const lowers to a zig fmt block" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const USAGE =
        \\    \\Usage: demo
        \\    \\
        \\    \\  -h  help
    );
    const want =
        \\const USAGE =
        \\    \\Usage: demo
        \\    \\
        \\    \\  -h  help
        \\;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "top-level `var` lowers to a file-scope mutable global, `const` stays immutable" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A file-scope `var` is a mutable global, the item-level binding with the
    // keyword flipped — `pub var` keeps the `pub`. A `const` global is unchanged.
    const got = try lowerSrc(a.allocator(),
        \\var counter usize = 0
        \\pub var ready bool = false
        \\const LIMIT usize = 10
    );
    const want =
        \\var counter: usize = 0;
        \\
        \\pub var ready: bool = false;
        \\
        \\const LIMIT: usize = 10;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "multiline string in binding and discard value position" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn show() {
        \\    msg :=
        \\        \\hi
        \\        \\there
        \\    _ = msg
        \\}
    );
    const want =
        \\fn show() void {
        \\    const msg =
        \\        \\hi
        \\        \\there
        \\    ;
        \\    _ = msg;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "sysinfo port: optional-capture if, decimal helper, for-over-bytes" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\use pwfile
        \\use console_ui
        \\use build_options
        \\
        \\link "flibc_start"
        \\link "flibc_mem"
        \\
        \\const PASSWD_MAX usize = build_options.passwd_max
        \\
        \\fn u64dec(out []mut u8, v u64) usize {
        \\    var x u64 = v
        \\    if x == 0 {
        \\        out[0] = '0'
        \\        return 1
        \\    }
        \\    var tmp [20]u8 = undefined
        \\    var n usize = 0
        \\    while x != 0 {
        \\        tmp[n] = '0' + #as(u8, #intCast(x % 10))
        \\        n += 1
        \\        x /= 10
        \\    }
        \\    var i usize = 0
        \\    while i < n {
        \\        out[i] = tmp[n - 1 - i]
        \\        i += 1
        \\    }
        \\    return n
        \\}
        \\
        \\fn currentUser(buf []mut u8) []u8 {
        \\    uid_raw := flibc.sys.getuid()
        \\    if uid_raw < 0 {
        \\        return "?"
        \\    }
        \\    uid := #as(u32, #intCast(uid_raw))
        \\    n := u64dec(buf, uid)
        \\    if pwfile.lookupByUid(buf[0..n], uid) |entry| {
        \\        return entry.user
        \\    }
        \\    return buf[0..n]
        \\}
        \\
        \\fn freePages(out []mut u8) usize {
        \\    pages := flibc.sys.dump_free()
        \\    var n usize = u64dec(out, pages)
        \\    suffix := " free"
        \\    for c in suffix {
        \\        out[n] = c
        \\        n += 1
        \\    }
        \\    return n
        \\}
        \\
        \\export fn main(_ usize, _ argv) noreturn {
        \\    console_ui.banner("sysinfo")
        \\    var ubuf [PASSWD_MAX]u8 = undefined
        \\    user := currentUser(&ubuf)
        \\    console_ui.screen.kv("user", user)
        \\    var fbuf [32]u8 = undefined
        \\    m := freePages(&fbuf)
        \\    console_ui.screen.kv("memory", fbuf[0..m])
        \\    flibc.exit()
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\const pwfile = @import("pwfile");
        \\const console_ui = @import("console_ui");
        \\const build_options = @import("build_options");
        \\
        \\comptime {
        \\    _ = @import("flibc_start");
        \\    _ = @import("flibc_mem");
        \\}
        \\
        \\const PASSWD_MAX: usize = build_options.passwd_max;
        \\
        \\fn u64dec(out: []u8, v: u64) usize {
        \\    var x: u64 = v;
        \\    if (x == 0) {
        \\        out[0] = '0';
        \\        return 1;
        \\    }
        \\    var tmp: [20]u8 = undefined;
        \\    var n: usize = 0;
        \\    while (x != 0) {
        \\        tmp[n] = '0' + @as(u8, @intCast(x % 10));
        \\        n += 1;
        \\        x /= 10;
        \\    }
        \\    var i: usize = 0;
        \\    while (i < n) {
        \\        out[i] = tmp[n - 1 - i];
        \\        i += 1;
        \\    }
        \\    return n;
        \\}
        \\
        \\fn currentUser(buf: []u8) []const u8 {
        \\    const uid_raw = flibc.sys.getuid();
        \\    if (uid_raw < 0) {
        \\        return "?";
        \\    }
        \\    const uid = @as(u32, @intCast(uid_raw));
        \\    const n = u64dec(buf, uid);
        \\    if (pwfile.lookupByUid(buf[0..n], uid)) |entry| {
        \\        return entry.user;
        \\    }
        \\    return buf[0..n];
        \\}
        \\
        \\fn freePages(out: []u8) usize {
        \\    const pages = flibc.sys.dump_free();
        \\    var n: usize = u64dec(out, pages);
        \\    const suffix = " free";
        \\    for (suffix) |c| {
        \\        out[n] = c;
        \\        n += 1;
        \\    }
        \\    return n;
        \\}
        \\
        \\export fn main(_: usize, _: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    console_ui.banner("sysinfo");
        \\    var ubuf: [PASSWD_MAX]u8 = undefined;
        \\    const user = currentUser(&ubuf);
        \\    console_ui.screen.kv("user", user);
        \\    var fbuf: [32]u8 = undefined;
        \\    const m = freePages(&fbuf);
        \\    console_ui.screen.kv("memory", fbuf[0..m]);
        \\    flibc.exit();
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "doc comments lower verbatim before their declarations" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A `///` block is kept byte-for-byte and re-emitted before the const, fn,
    // struct field/member, and enum/union variant it leads — the content after
    // each `///` (the leading space included) is preserved. This output is
    // byte-identical to `zig fmt` of itself and passes `zig ast-check`.
    const got = try lowerSrc(a.allocator(),
        \\/// The maximum number of entries.
        \\/// Tunable at build time.
        \\pub const MAX usize = 16
        \\
        \\/// Returns the smaller of two values.
        \\inline fn min(a usize, b usize) usize {
        \\    return a
        \\}
        \\
        \\const Point = struct {
        \\    /// The horizontal coordinate.
        \\    x i32,
        \\    y i32,
        \\
        \\    /// Manhattan distance from the origin.
        \\    pub fn norm(self Point) i32 {
        \\        return self.x + self.y
        \\    }
        \\
        \\    /// The origin point.
        \\    const ZERO i32 = 0
        \\}
        \\
        \\const Kind = enum(u8) {
        \\    /// A regular file.
        \\    file,
        \\    dir,
        \\}
        \\
        \\const Tok = union(enum) {
        \\    /// End of input.
        \\    eof,
        \\    int usize,
        \\}
    );
    const want =
        \\/// The maximum number of entries.
        \\/// Tunable at build time.
        \\pub const MAX: usize = 16;
        \\
        \\/// Returns the smaller of two values.
        \\inline fn min(a: usize, b: usize) usize {
        \\    return a;
        \\}
        \\
        \\const Point = struct {
        \\    /// The horizontal coordinate.
        \\    x: i32,
        \\    y: i32,
        \\
        \\    /// Manhattan distance from the origin.
        \\    pub fn norm(self: Point) i32 {
        \\        return self.x + self.y;
        \\    }
        \\
        \\    /// The origin point.
        \\    const ZERO: i32 = 0;
        \\};
        \\
        \\const Kind = enum(u8) {
        \\    /// A regular file.
        \\    file,
        \\    dir,
        \\};
        \\
        \\const Tok = union(enum) {
        \\    /// End of input.
        \\    eof,
        \\    int: usize,
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "readline port: step's switch — value list, range, if-expr arm, block arms, typed literal" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The centerpiece of the flibc readline core, ported from hand-written Zig:
    // a `switch` over the input byte with a value list (`'\r', '\n'`), an
    // inclusive range (`0x20...0x7e`), an inline `if`-expression arm, two
    // `blk: { … }` block arms whose value is a `break :blk …`, a typed union
    // literal (`Action{ .echo = byte }`), and struct field defaults. The braceless
    // single-statement `if` bodies of the reference become Flash's mandatory
    // braces (a canonical normalization); otherwise the token stream matches. The
    // full module lives in examples/readline.flash.
    const got = try lowerSrc(a.allocator(),
        \\pub const State = struct {
        \\    buf []mut u8,
        \\    len usize = 0,
        \\    pos usize = 0,
        \\}
        \\
        \\pub const Action = union(enum) {
        \\    none,
        \\    echo u8,
        \\    backspace,
        \\    submit,
        \\    complete,
        \\    abandon,
        \\    eof,
        \\}
        \\
        \\/// One-byte state transition for the plain (append-only) editor. Pure: no
        \\/// syscalls, no allocator.
        \\pub fn step(state *mut State, byte u8) Action {
        \\    return switch byte {
        \\        '\r', '\n' => .submit,
        \\        0x03 => .abandon,
        \\        0x04 => if (state.len == 0) Action.eof else Action.none,
        \\        0x09 => .complete,
        \\        0x08, 0x7f => blk: {
        \\            if state.len == 0 {
        \\                break :blk Action.none
        \\            }
        \\            state.len -= 1
        \\            break :blk Action.backspace
        \\        },
        \\        0x20...0x7e => blk: {
        \\            if state.len >= state.buf.len {
        \\                break :blk Action.none
        \\            }
        \\            state.buf[state.len] = byte
        \\            state.len += 1
        \\            break :blk Action{ .echo = byte }
        \\        },
        \\        else => .none,
        \\    }
        \\}
    );
    const want =
        \\pub const State = struct {
        \\    buf: []u8,
        \\    len: usize = 0,
        \\    pos: usize = 0,
        \\};
        \\
        \\pub const Action = union(enum) {
        \\    none,
        \\    echo: u8,
        \\    backspace,
        \\    submit,
        \\    complete,
        \\    abandon,
        \\    eof,
        \\};
        \\
        \\/// One-byte state transition for the plain (append-only) editor. Pure: no
        \\/// syscalls, no allocator.
        \\pub fn step(state: *State, byte: u8) Action {
        \\    return switch (byte) {
        \\        '\r', '\n' => .submit,
        \\        0x03 => .abandon,
        \\        0x04 => if (state.len == 0) Action.eof else Action.none,
        \\        0x09 => .complete,
        \\        0x08, 0x7f => blk: {
        \\            if (state.len == 0) {
        \\                break :blk Action.none;
        \\            }
        \\            state.len -= 1;
        \\            break :blk Action.backspace;
        \\        },
        \\        0x20...0x7e => blk: {
        \\            if (state.len >= state.buf.len) {
        \\                break :blk Action.none;
        \\            }
        \\            state.buf[state.len] = byte;
        \\            state.len += 1;
        \\            break :blk Action{ .echo = byte };
        \\        },
        \\        else => .none,
        \\    };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "tokenize port: union(enum) result, union/enum-literal returns, sentinel slice, doc comments" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The fsh command tokenizer ported from hand-written Zig: a tagged-union
    // `Result`, union literals (including a nested `.{ .piped = .{ … } }`), bare
    // enum-literal returns, the composite `*mut [MAX_ARGS]?[*:0]u8` signature, a
    // sentinel slice, an open-ended chained slice, and two compound-condition
    // `while` scans. `///` doc comments carry through verbatim. The output is
    // byte-identical to `zig fmt` of itself and passes `zig ast-check`.
    const got = try lowerSrc(a.allocator(),
        \\/// argv capacity, including the interleaved `null` separators (the pipe
        \\/// boundary and the trailing terminator). 16 covers a command plus a
        \\/// generous argument list for demoware; longer lines truncate.
        \\pub const MAX_ARGS usize = 16
        \\
        \\/// Why the two sides of a `|` cannot both be commands, or why a second
        \\/// `|` appeared.
        \\pub const Err = enum {
        \\    too_many_pipes,
        \\    empty_side,
        \\}
        \\
        \\/// A single-pipe decomposition. The right command's argv begins at
        \\/// `argv[left_argc + 1]` (the `+ 1` skips the `null` the tokenizer wrote
        \\/// at the pipe boundary); both vectors are NULL-terminated in place.
        \\pub const Piped = struct {
        \\    left_argc usize,
        \\    right_argc usize,
        \\}
        \\
        \\/// How a line decomposed.
        \\pub const Result = union(enum) {
        \\    /// Blank or whitespace-only line — fsh redraws the prompt.
        \\    empty,
        \\    /// One command; `argv[0..argc]` valid, `argv[argc] == null`.
        \\    single usize,
        \\    /// One pipe stage; see `Piped`.
        \\    piped Piped,
        \\    /// Malformed pipe usage.
        \\    err Err,
        \\}
        \\
        \\inline fn is_space(c u8) bool {
        \\    return c == ' ' || c == '\t' || c == '\r' || c == '\n'
        \\}
        \\
        \\/// Split `line` into `argv` (pointers into `buf`). See the module header
        \\/// for the decomposition rules. `argv` and `buf` are caller-owned and
        \\/// reused per line; the returned pointers are valid until the next call
        \\/// that reuses them.
        \\pub fn tokenize(line []u8, argv *mut [MAX_ARGS]?[*:0]mut u8, buf []mut u8) Result {
        \\    var argc usize = 0
        \\    var buf_pos usize = 0
        \\    var pipe_at ?usize = null
        \\    var pipes usize = 0
        \\
        \\    var i usize = 0
        \\    while i < line.len {
        \\        while i < line.len && is_space(line[i]) {
        \\            i += 1
        \\        }
        \\        if i >= line.len {
        \\            break
        \\        }
        \\
        \\        if argc >= MAX_ARGS - 1 {
        \\            break
        \\        }
        \\
        \\        if line[i] == '|' {
        \\            pipes += 1
        \\            if pipes > 1 {
        \\                return .{ .err = .too_many_pipes }
        \\            }
        \\            pipe_at = argc
        \\            argv[argc] = null
        \\            argc += 1
        \\            i += 1
        \\            continue
        \\        }
        \\
        \\        start := i
        \\        while i < line.len && !is_space(line[i]) && line[i] != '|' {
        \\            i += 1
        \\        }
        \\        tok := line[start..i]
        \\
        \\        if buf_pos + tok.len + 1 > buf.len {
        \\            break
        \\        }
        \\        #memcpy(buf[buf_pos..][0..tok.len], tok)
        \\        buf[buf_pos + tok.len] = 0
        \\        argv[argc] = buf[buf_pos .. buf_pos + tok.len :0].ptr
        \\        argc += 1
        \\        buf_pos += tok.len + 1
        \\    }
        \\
        \\    if argc < MAX_ARGS {
        \\        argv[argc] = null
        \\    }
        \\
        \\    if pipe_at |p| {
        \\        left_argc := p
        \\        right_argc := argc - p - 1
        \\        if left_argc == 0 || right_argc == 0 {
        \\            return .{ .err = .empty_side }
        \\        }
        \\        return .{ .piped = .{ .left_argc = left_argc, .right_argc = right_argc } }
        \\    }
        \\
        \\    if argc == 0 {
        \\        return .empty
        \\    }
        \\    return .{ .single = argc }
        \\}
    );
    const want =
        \\/// argv capacity, including the interleaved `null` separators (the pipe
        \\/// boundary and the trailing terminator). 16 covers a command plus a
        \\/// generous argument list for demoware; longer lines truncate.
        \\pub const MAX_ARGS: usize = 16;
        \\
        \\/// Why the two sides of a `|` cannot both be commands, or why a second
        \\/// `|` appeared.
        \\pub const Err = enum {
        \\    too_many_pipes,
        \\    empty_side,
        \\};
        \\
        \\/// A single-pipe decomposition. The right command's argv begins at
        \\/// `argv[left_argc + 1]` (the `+ 1` skips the `null` the tokenizer wrote
        \\/// at the pipe boundary); both vectors are NULL-terminated in place.
        \\pub const Piped = struct {
        \\    left_argc: usize,
        \\    right_argc: usize,
        \\};
        \\
        \\/// How a line decomposed.
        \\pub const Result = union(enum) {
        \\    /// Blank or whitespace-only line — fsh redraws the prompt.
        \\    empty,
        \\    /// One command; `argv[0..argc]` valid, `argv[argc] == null`.
        \\    single: usize,
        \\    /// One pipe stage; see `Piped`.
        \\    piped: Piped,
        \\    /// Malformed pipe usage.
        \\    err: Err,
        \\};
        \\
        \\inline fn is_space(c: u8) bool {
        \\    return c == ' ' or c == '\t' or c == '\r' or c == '\n';
        \\}
        \\
        \\/// Split `line` into `argv` (pointers into `buf`). See the module header
        \\/// for the decomposition rules. `argv` and `buf` are caller-owned and
        \\/// reused per line; the returned pointers are valid until the next call
        \\/// that reuses them.
        \\pub fn tokenize(line: []const u8, argv: *[MAX_ARGS]?[*:0]u8, buf: []u8) Result {
        \\    var argc: usize = 0;
        \\    var buf_pos: usize = 0;
        \\    var pipe_at: ?usize = null;
        \\    var pipes: usize = 0;
        \\    var i: usize = 0;
        \\    while (i < line.len) {
        \\        while (i < line.len and is_space(line[i])) {
        \\            i += 1;
        \\        }
        \\        if (i >= line.len) {
        \\            break;
        \\        }
        \\        if (argc >= MAX_ARGS - 1) {
        \\            break;
        \\        }
        \\        if (line[i] == '|') {
        \\            pipes += 1;
        \\            if (pipes > 1) {
        \\                return .{ .err = .too_many_pipes };
        \\            }
        \\            pipe_at = argc;
        \\            argv[argc] = null;
        \\            argc += 1;
        \\            i += 1;
        \\            continue;
        \\        }
        \\        const start = i;
        \\        while (i < line.len and !is_space(line[i]) and line[i] != '|') {
        \\            i += 1;
        \\        }
        \\        const tok = line[start..i];
        \\        if (buf_pos + tok.len + 1 > buf.len) {
        \\            break;
        \\        }
        \\        @memcpy(buf[buf_pos..][0..tok.len], tok);
        \\        buf[buf_pos + tok.len] = 0;
        \\        argv[argc] = buf[buf_pos .. buf_pos + tok.len :0].ptr;
        \\        argc += 1;
        \\        buf_pos += tok.len + 1;
        \\    }
        \\    if (argc < MAX_ARGS) {
        \\        argv[argc] = null;
        \\    }
        \\    if (pipe_at) |p| {
        \\        const left_argc = p;
        \\        const right_argc = argc - p - 1;
        \\        if (left_argc == 0 or right_argc == 0) {
        \\            return .{ .err = .empty_side };
        \\        }
        \\        return .{ .piped = .{ .left_argc = left_argc, .right_argc = right_argc } };
        \\    }
        \\    if (argc == 0) {
        \\        return .empty;
        \\    }
        \\    return .{ .single = argc };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "many-item pointers are const by default; `mut` opts the pointee into mutability" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A many-item pointer is const-pointee by default in Flash, exactly like a
    // slice (`[]T`) and a single pointer (`*T`); `mut` opts the pointee in. So the
    // plain `[*]T` / `[*:0]T` gain an implicit `const` in the Zig output while the
    // `[*]mut T` / `[*:0]mut T` forms drop it — each round-trips verbatim.
    const got = try lowerSrc(a.allocator(),
        \\fn f(a [*]u8, b [*]mut u8, c [*:0]u8, d [*:0]mut u8) {
        \\    _ = a
        \\    _ = b
        \\    _ = c
        \\    _ = d
        \\}
    );
    const want =
        \\fn f(a: [*]const u8, b: [*]u8, c: [*:0]const u8, d: [*:0]u8) void {
        \\    _ = a;
        \\    _ = b;
        \\    _ = c;
        \\    _ = d;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "mem port: C-ABI mem*/strlen providers — const many-ptr sources, sentinel-const scan" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The flibc `mem*` providers, ported from hand-written Zig: three C-ABI
    // `export fn`s LLVM's loop-idiom recognizer needs to find by name. memcpy's
    // 8-at-a-time fast path is the centerpiece — `[*]const u8` / `[*]const u64`
    // copy sources, an `&&` alignment guard lowering to `and`, and
    // `#ptrCast`/`#alignCast`/`#intFromPtr` builtins; strlen scans a
    // `[*:0]const u8`. Every `export fn` gains the explicit `callconv(.c)` C-ABI
    // boundary marker. The full module lives in examples/mem.flash.
    const got = try lowerSrc(a.allocator(),
        \\/// memset(dst, c, n) — fill `n` bytes of `dst` with byte `c`. Byte
        \\/// granular; the C ABI returns `dst`.
        \\export fn memset(dst [*]mut u8, c i32, n_in u64) [*]mut u8 {
        \\    var n = n_in
        \\    var p = dst
        \\    const byte u8 = #truncate(#as(u32, #bitCast(c)))
        \\    while n != 0 {
        \\        p[0] = byte
        \\        p += 1
        \\        n -= 1
        \\    }
        \\    return dst
        \\}
        \\
        \\/// memcpy(dst, src, bytes) — copy `bytes` bytes from `src` to `dst`
        \\/// (non-overlapping). Copies 8 bytes at a time when both operands are
        \\/// 8-aligned, then drains the tail byte-wise. The C ABI returns `dst`.
        \\export fn memcpy(dst *mut anyopaque, src *anyopaque, bytes u64) *mut anyopaque {
        \\    var d [*]mut u8 = #ptrCast(dst)
        \\    var s [*]u8 = #ptrCast(src)
        \\    var n = bytes
        \\
        \\    if #intFromPtr(d) % 8 == 0 && #intFromPtr(s) % 8 == 0 {
        \\        var d64 [*]mut u64 = #ptrCast(#alignCast(d))
        \\        var s64 [*]u64 = #ptrCast(#alignCast(s))
        \\        while n >= 8 {
        \\            d64[0] = s64[0]
        \\            d64 += 1
        \\            s64 += 1
        \\            n -= 8
        \\        }
        \\        d = #ptrCast(d64)
        \\        s = #ptrCast(s64)
        \\    }
        \\
        \\    while n > 0 {
        \\        d[0] = s[0]
        \\        d += 1
        \\        s += 1
        \\        n -= 1
        \\    }
        \\    return dst
        \\}
        \\
        \\/// strlen(s) — length of the NUL-terminated string at `s`, excluding the
        \\/// terminator. The lone scan the idiom recognizer would otherwise route
        \\/// to an external `strlen`; defining it here closes the loop.
        \\export fn strlen(s [*:0]u8) u64 {
        \\    var n u64 = 0
        \\    while s[n] != 0 {
        \\        n += 1
        \\    }
        \\    return n
        \\}
    );
    const want =
        \\/// memset(dst, c, n) — fill `n` bytes of `dst` with byte `c`. Byte
        \\/// granular; the C ABI returns `dst`.
        \\export fn memset(dst: [*]u8, c: i32, n_in: u64) callconv(.c) [*]u8 {
        \\    var n = n_in;
        \\    var p = dst;
        \\    const byte: u8 = @truncate(@as(u32, @bitCast(c)));
        \\    while (n != 0) {
        \\        p[0] = byte;
        \\        p += 1;
        \\        n -= 1;
        \\    }
        \\    return dst;
        \\}
        \\
        \\/// memcpy(dst, src, bytes) — copy `bytes` bytes from `src` to `dst`
        \\/// (non-overlapping). Copies 8 bytes at a time when both operands are
        \\/// 8-aligned, then drains the tail byte-wise. The C ABI returns `dst`.
        \\export fn memcpy(dst: *anyopaque, src: *const anyopaque, bytes: u64) callconv(.c) *anyopaque {
        \\    var d: [*]u8 = @ptrCast(dst);
        \\    var s: [*]const u8 = @ptrCast(src);
        \\    var n = bytes;
        \\    if (@intFromPtr(d) % 8 == 0 and @intFromPtr(s) % 8 == 0) {
        \\        var d64: [*]u64 = @ptrCast(@alignCast(d));
        \\        var s64: [*]const u64 = @ptrCast(@alignCast(s));
        \\        while (n >= 8) {
        \\            d64[0] = s64[0];
        \\            d64 += 1;
        \\            s64 += 1;
        \\            n -= 8;
        \\        }
        \\        d = @ptrCast(d64);
        \\        s = @ptrCast(s64);
        \\    }
        \\    while (n > 0) {
        \\        d[0] = s[0];
        \\        d += 1;
        \\        s += 1;
        \\        n -= 1;
        \\    }
        \\    return dst;
        \\}
        \\
        \\/// strlen(s) — length of the NUL-terminated string at `s`, excluding the
        \\/// terminator. The lone scan the idiom recognizer would otherwise route
        \\/// to an external `strlen`; defining it here closes the loop.
        \\export fn strlen(s: [*:0]const u8) callconv(.c) u64 {
        \\    var n: u64 = 0;
        \\    while (s[n] != 0) {
        \\        n += 1;
        \\    }
        \\    return n;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "start port: extern fn prototype, explicit callconv, and a comptime #export block" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The flibc `_start` argv shim, ported from hand-written Zig — the first
    // port to need three grammar forms: a bodyless `extern fn` prototype
    // (closing with `;`, no block), an explicit `callconv(.c)` written in the
    // signature, and a top-level `comptime { … }` block. The `argv` spelling
    // lowers to the argv pointer type. The full module is examples/start.flash.
    const got = try lowerSrc(a.allocator(),
        \\extern fn main(argc usize, argv argv) callconv(.c) noreturn
        \\
        \\fn _start_shim(argc usize, argv argv) callconv(.c) noreturn {
        \\    main(argc, argv)
        \\}
        \\
        \\comptime {
        \\    #export(&_start_shim, .{ .name = "_start", .linkage = .strong })
        \\}
    );
    const want =
        \\extern fn main(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn;
        \\
        \\fn _start_shim(argc: usize, argv: [*]const ?[*:0]const u8) callconv(.c) noreturn {
        \\    main(argc, argv);
        \\}
        \\
        \\comptime {
        \\    @export(&_start_shim, .{ .name = "_start", .linkage = .strong });
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "file imports: a quoted extensionless `use \"x\"` lowers to a sibling-file @import" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // A quoted target is a sibling-file import: the source names the stem only and
    // lowering supplies the backend `.zig`, distinct from the bare-name module
    // import. Both fold into the same run of consecutive `use` declarations, one
    // `const … = @import(…)` per line.
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\use "syscalls" as sys
    );
    const want =
        \\const flibc = @import("flibc");
        \\const sys = @import("syscalls.zig");
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "a `use` inside a struct body lowers to an indented struct-level @import" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The struct-level `use` lowers to the same `const … = @import(…)` as the
    // top-level form, just indented one level — replacing the in-struct
    // `const sys = #import(…)` workaround so `use` is the one import spelling.
    // A quoted import is extensionless; lowering adds the backend `.zig`.
    const got = try lowerSrc(a.allocator(),
        \\const driver = struct {
        \\    use "syscalls" as sys
        \\
        \\    pub fn run() {}
        \\}
    );
    const want =
        \\const driver = struct {
        \\    const sys = @import("syscalls.zig");
        \\
        \\    pub fn run() void {}
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "process port: flibc process glue over a sibling-file syscalls import" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // The flibc process-glue layer, ported from hand-written Zig — the first
    // port to use a sibling file import (`use "syscalls" as sys`). The five
    // wrappers are thin C-ABI passthroughs; `exit` returns `noreturn` and its
    // body is a bare call, and the pointer params use the `cstr` / `argv`
    // aliases. The full module lives in examples/process.flash.
    const got = try lowerSrc(a.allocator(),
        \\use "syscalls" as sys
        \\
        \\/// fork() — clone the current process. Returns the child's pid in the
        \\/// parent and 0 in the child. -1 on failure (NR_TASKS exhausted,
        \\/// out-of-memory, etc.).
        \\pub fn fork() i32 {
        \\    return sys.fork()
        \\}
        \\
        \\/// exit() — terminate the current process. Never returns. The kernel
        \\/// flips the task to TASK_ZOMBIE; the parent's wait reaps it (frees
        \\/// every user/kernel page tracked by `mm`).
        \\pub fn exit() noreturn {
        \\    sys.exit()
        \\}
        \\
        \\/// execve(path, argv) — path-resolved exec on slot 31. `path` is a
        \\/// NUL-terminated UVA; `argv` points at a NULL-terminated array of
        \\/// `[*:0]u8`. Returns -1 on failure with the address space untouched.
        \\pub fn execve(path cstr, argv argv) i32 {
        \\    return sys.exec_path(path, argv)
        \\}
    );
    const want =
        \\const sys = @import("syscalls.zig");
        \\
        \\/// fork() — clone the current process. Returns the child's pid in the
        \\/// parent and 0 in the child. -1 on failure (NR_TASKS exhausted,
        \\/// out-of-memory, etc.).
        \\pub fn fork() i32 {
        \\    return sys.fork();
        \\}
        \\
        \\/// exit() — terminate the current process. Never returns. The kernel
        \\/// flips the task to TASK_ZOMBIE; the parent's wait reaps it (frees
        \\/// every user/kernel page tracked by `mm`).
        \\pub fn exit() noreturn {
        \\    sys.exit();
        \\}
        \\
        \\/// execve(path, argv) — path-resolved exec on slot 31. `path` is a
        \\/// NUL-terminated UVA; `argv` points at a NULL-terminated array of
        \\/// `[*:0]u8`. Returns -1 on failure with the address space untouched.
        \\pub fn execve(path: [*:0]const u8, argv: [*]const ?[*:0]const u8) i32 {
        \\    return sys.exec_path(path, argv);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "heap port: bump allocator — unary `~` alignment mask, optional many-ptr, empty no-op body" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's heap layer, ported from hand-written Zig. The port that surfaces
    // the unary bitwise-NOT `~`, used in the 8-byte alignment mask
    // `~(ALIGN - 1)`. `malloc` returns an optional many-item pointer
    // (`?[*]u8`); `free` is an empty no-op body with an ignored `_` param. The
    // grouping parens are preserved verbatim and the two single-statement `if`s
    // lower with mandatory braces. The full module lives in examples/heap.flash.
    const got = try lowerSrc(a.allocator(),
        \\use "syscalls" as sys
        \\
        \\const ALIGN u64 = 8
        \\
        \\/// malloc(n) — return a pointer to a freshly-allocated region of at
        \\/// least `n` bytes (rounded up to 8). Returns null on failure
        \\/// (kernel rejects out-of-bounds break, propagated as a negative sbrk
        \\/// return). The memory is zeroed by the kernel's get_free_page on first
        \\/// touch via the do_data_abort demand-alloc path.
        \\///
        \\/// C `malloc(0)` is implementation-defined; flibc returns null.
        \\/// Callers must distinguish `len == 0` themselves before treating
        \\/// null as failure.
        \\pub fn malloc(n u64) ?[*]mut u8 {
        \\    if n == 0 { return null }
        \\    const aligned u64 = (n + ALIGN - 1) & ~(ALIGN - 1)
        \\    const prev = sys.sbrk(#intCast(aligned))
        \\    if prev < 0 { return null }
        \\    return #ptrFromInt(#as(u64, #bitCast(prev)))
        \\}
        \\
        \\/// free — no-op. The bump allocator never reclaims individual
        \\/// allocations; the kernel reaps the entire heap on process exit
        \\/// (do_wait clears every page in `mm.user_pages`). Provided so consumers
        \\/// can keep the alloc/free pairing readable even though the call is
        \\/// inert.
        \\pub fn free(_ ?[*]mut u8) {}
    );
    const want =
        \\const sys = @import("syscalls.zig");
        \\
        \\const ALIGN: u64 = 8;
        \\
        \\/// malloc(n) — return a pointer to a freshly-allocated region of at
        \\/// least `n` bytes (rounded up to 8). Returns null on failure
        \\/// (kernel rejects out-of-bounds break, propagated as a negative sbrk
        \\/// return). The memory is zeroed by the kernel's get_free_page on first
        \\/// touch via the do_data_abort demand-alloc path.
        \\///
        \\/// C `malloc(0)` is implementation-defined; flibc returns null.
        \\/// Callers must distinguish `len == 0` themselves before treating
        \\/// null as failure.
        \\pub fn malloc(n: u64) ?[*]u8 {
        \\    if (n == 0) {
        \\        return null;
        \\    }
        \\    const aligned: u64 = (n + ALIGN - 1) & ~(ALIGN - 1);
        \\    const prev = sys.sbrk(@intCast(aligned));
        \\    if (prev < 0) {
        \\        return null;
        \\    }
        \\    return @ptrFromInt(@as(u64, @bitCast(prev)));
        \\}
        \\
        \\/// free — no-op. The bump allocator never reclaims individual
        \\/// allocations; the kernel reaps the entire heap on process exit
        \\/// (do_wait clears every page in `mm.user_pages`). Provided so consumers
        \\/// can keep the alloc/free pairing readable even though the call is
        \\/// inert.
        \\pub fn free(_: ?[*]u8) void {}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "flibc port: `pub use` re-exports lower to `pub const … = @import`, interleaved with value re-exports" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's re-export hub, ported from hand-written Zig. The port that
    // surfaces `pub use`: a re-exported import (`pub use "io" as io` →
    // `pub const io = @import("io.zig")`), packed in a run with the bare-module
    // `use syscall_defs as defs`. The value re-exports (`pub const printf =
    // io.printf`) are ordinary `pub const`s over a member-access expression.
    // The full 35-declaration module lives in examples/flibc.flash; its
    // declaration stream is token-identical to the reference, modulo the dropped
    // `//` comments and Flash's uniform one-blank-per-declaration layout.
    const got = try lowerSrc(a.allocator(),
        \\pub use "syscalls" as sys
        \\pub use "io" as io
        \\use syscall_defs as defs
        \\pub const Dirent = defs.Dirent
        \\pub const printf = io.printf
        \\pub use "heap" as heap
        \\pub const malloc = heap.malloc
    );
    const want =
        \\pub const sys = @import("syscalls.zig");
        \\pub const io = @import("io.zig");
        \\const defs = @import("syscall_defs");
        \\
        \\pub const Dirent = defs.Dirent;
        \\
        \\pub const printf = io.printf;
        \\
        \\pub const heap = @import("heap.zig");
        \\
        \\pub const malloc = heap.malloc;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "execvp port: sentinel-slice return type and the if-expression driver select" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's bare-name program resolver, ported from hand-written Zig. The port
    // that surfaces the sentinel-terminated *slice* type — `?[:0]mut u8`, a
    // mutable `[:0]u8`, lowering to `?[:0]u8` — and the driver select: a comptime
    // `if`-expression whose two `struct { … }` arms pick the real aarch64 driver
    // or the host stub, so the off-target SVC path is never analysed. The
    // in-struct sibling import (`use "syscalls" as sys`) lowers to a struct-level
    // `const sys = @import("syscalls.zig")` — the same `use` form as at the top
    // level, just indented; the `cstr` / `argv` spelling aliases stand in for the
    // two C-string pointer types. The full module lives in examples/execvp.flash; its core is
    // token-identical to the reference, modulo the mandatory braces on the
    // single-statement `if`s. The `&&` operator lowers to `and`.
    const got = try lowerSrc(a.allocator(),
        \\use builtin
        \\
        \\const has_driver = builtin.cpu.arch == .aarch64 && builtin.target.os.tag == .freestanding
        \\
        \\pub fn resolve(name []u8, out []mut u8) ?[:0]mut u8 {
        \\    if name.len == 0 { return null }
        \\    out[name.len] = 0
        \\    return out[0..name.len :0]
        \\}
        \\
        \\pub const execvp = driver.execvp
        \\
        \\const driver = if (has_driver) struct {
        \\    use "syscalls" as sys
        \\
        \\    pub fn execvp(name cstr, argv argv) i32 {
        \\        return sys.exec_path(name, argv)
        \\    }
        \\} else struct {
        \\    pub fn execvp(_ cstr, _ argv) i32 {
        \\        return -1
        \\    }
        \\}
    );
    const want =
        \\const builtin = @import("builtin");
        \\
        \\const has_driver = builtin.cpu.arch == .aarch64 and builtin.target.os.tag == .freestanding;
        \\
        \\pub fn resolve(name: []const u8, out: []u8) ?[:0]u8 {
        \\    if (name.len == 0) {
        \\        return null;
        \\    }
        \\    out[name.len] = 0;
        \\    return out[0..name.len :0];
        \\}
        \\
        \\pub const execvp = driver.execvp;
        \\
        \\const driver = if (has_driver) struct {
        \\    const sys = @import("syscalls.zig");
        \\
        \\    pub fn execvp(name: [*:0]const u8, argv: [*]const ?[*:0]const u8) i32 {
        \\        return sys.exec_path(name, argv);
        \\    }
        \\} else struct {
        \\    pub fn execvp(_: [*:0]const u8, _: [*]const ?[*:0]const u8) i32 {
        \\        return -1;
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "io port: comptime-format printf — comptime params/vars, inline while, +%/++/[_]u8 literal" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's console I/O layer, ported from hand-written Zig. The port that
    // surfaces the comptime-format machinery: a `comptime fmt` parameter with
    // `args anytype`, `comptime var` walk counters, an `inline while` over the
    // format string, the wrapping add `+%`, array/string concat `++`, and an
    // inferred-length array literal `&[_]u8{spec}` inside `#compileError`. The
    // `switch` used as a bare statement takes no trailing `;` (a Zig block-form
    // statement). The full module lives in examples/io.flash; its core lowers
    // token-identical to the reference.
    const got = try lowerSrc(a.allocator(),
        \\pub fn printf(comptime fmt []u8, args anytype) {
        \\    comptime var i usize = 0
        \\    inline while i < fmt.len {
        \\        const c = fmt[i]
        \\        const m = c +% 1
        \\        emit(m, args[i])
        \\        i += 1
        \\    }
        \\}
        \\
        \\inline fn emit(comptime spec u8, arg anytype) {
        \\    switch spec {
        \\        'x' => put(arg),
        \\        else => #compileError("bad %" ++ &[_]u8{spec}),
        \\    }
        \\}
    );
    const want =
        \\pub fn printf(comptime fmt: []const u8, args: anytype) void {
        \\    comptime var i: usize = 0;
        \\    inline while (i < fmt.len) {
        \\        const c = fmt[i];
        \\        const m = c +% 1;
        \\        emit(m, args[i]);
        \\        i += 1;
        \\    }
        \\}
        \\
        \\inline fn emit(comptime spec: u8, arg: anytype) void {
        \\    switch (spec) {
        \\        'x' => put(arg),
        \\        else => @compileError("bad %" ++ &[_]u8{spec}),
        \\    }
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "comptime binding: `comptime const` wraps its value, `comptime var` keeps the prefix" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // Zig rejects `comptime const` as redundant — its own diagnostic directs
    // the comptime-ness onto the initializer ("wrap the initialization
    // expression with 'comptime'"). So a comptime immutable binding lowers to
    // `const x = comptime e`, preserving the force-comptime intent on the value
    // rather than emitting the invalid `comptime const x = e`. A `comptime var`
    // is valid Zig and keeps its prefix unchanged.
    const got = try lowerSrc(a.allocator(),
        \\pub fn f() {
        \\    comptime const N = 4
        \\    comptime var i usize = 0
        \\    i += N
        \\}
    );
    const want =
        \\pub fn f() void {
        \\    const N = comptime 4;
        \\    comptime var i: usize = 0;
        \\    i += N;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "keys port: VT100 decoder — switch ranges, multi-pattern and labeled-block prongs, driver select" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's console key decoder, ported from hand-written Zig. The first pure
    // port: it adds no new grammar. This subset exercises the combination the
    // reference leans on — a struct with a defaulted field and a nested `const`
    // enum, a method over `*mut Decoder` whose `return switch` body carries a
    // labeled-block prong (`blk: { … break :blk … }`), a multi-pattern prong
    // (`'\r', '\n' =>`), an inclusive range prong (`0x20...0x7e =>`), the
    // comptime gate `&&` (lowering to `and`), and the driver-select
    // `if (has_driver) struct {…} else struct {…}`. A brace-less single-statement
    // `if` gains its mandatory braces. The full module lives in
    // examples/keys.flash; its core lowers token-identical to the reference.
    const got = try lowerSrc(a.allocator(),
        \\const has_driver = builtin.cpu.arch == .aarch64 && builtin.target.os.tag == .freestanding
        \\
        \\pub const Decoder = struct {
        \\    state State = .ground,
        \\
        \\    const State = enum { ground, esc, csi }
        \\
        \\    fn atGround(self *mut Decoder, b u8) Event {
        \\        return switch b {
        \\            0x1b => blk: {
        \\                self.state = .esc
        \\                break :blk .{ .key = .none }
        \\            },
        \\            '\r', '\n' => .{ .key = .enter },
        \\            0x20...0x7e => .{ .key = .char, .ch = b },
        \\            else => .{ .key = .none },
        \\        }
        \\    }
        \\}
        \\
        \\const driver = if (has_driver) struct {
        \\    pub fn readKey() Event {
        \\        var b u8 = 0
        \\        if (b >= '0' && b <= '9') || b == 0 {
        \\            return .{ .key = .eof }
        \\        }
        \\        return .{ .key = .none }
        \\    }
        \\} else struct {
        \\    pub fn readKey() Event {
        \\        return .{ .key = .eof }
        \\    }
        \\}
    );
    const want =
        \\const has_driver = builtin.cpu.arch == .aarch64 and builtin.target.os.tag == .freestanding;
        \\
        \\pub const Decoder = struct {
        \\    state: State = .ground,
        \\
        \\    const State = enum {
        \\        ground,
        \\        esc,
        \\        csi,
        \\    };
        \\
        \\    fn atGround(self: *Decoder, b: u8) Event {
        \\        return switch (b) {
        \\            0x1b => blk: {
        \\                self.state = .esc;
        \\                break :blk .{ .key = .none };
        \\            },
        \\            '\r', '\n' => .{ .key = .enter },
        \\            0x20...0x7e => .{ .key = .char, .ch = b },
        \\            else => .{ .key = .none },
        \\        };
        \\    }
        \\};
        \\
        \\const driver = if (has_driver) struct {
        \\    pub fn readKey() Event {
        \\        var b: u8 = 0;
        \\        if ((b >= '0' and b <= '9') or b == 0) {
        \\            return .{ .key = .eof };
        \\        }
        \\        return .{ .key = .none };
        \\    }
        \\} else struct {
        \\    pub fn readKey() Event {
        \\        return .{ .key = .eof };
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "completion port: parenthesised value-if condition, optional-capture, range-for" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's tab-completion core, ported from hand-written Zig. The second pure
    // port — but it surfaces one grammar gap: a value `if`-expression whose
    // then-arm is a `.enumLiteral` needs its condition parenthesised
    // (`if (best > typed) .progressed else .stuck`), so the `.progressed` is not
    // glued onto the condition's tail as a member access. The lowered Zig keeps
    // the single condition parens (no doubling). The rest reuses landed surface:
    // an optional-capture `if (slash) |s| { … }`, a `?usize` optional, a
    // range-`for` loop (`for (0..line.len)`), and Flash's mandatory braces on the
    // single-statement `if`. The full module lives in examples/completion.flash;
    // its core lowers token-identical to the reference.
    const got = try lowerSrc(a.allocator(),
        \\pub fn classify(count usize, best usize, typed usize) Tab {
        \\    if count == 0 {
        \\        return .empty
        \\    }
        \\    return if (best > typed) .progressed else .stuck
        \\}
        \\
        \\pub fn split(line []u8) ?usize {
        \\    var slash ?usize = null
        \\    for i in 0..line.len {
        \\        if line[i] == '/' {
        \\            slash = i
        \\        }
        \\    }
        \\    if slash |s| {
        \\        return s
        \\    }
        \\    return null
        \\}
    );
    const want =
        \\pub fn classify(count: usize, best: usize, typed: usize) Tab {
        \\    if (count == 0) {
        \\        return .empty;
        \\    }
        \\    return if (best > typed) .progressed else .stuck;
        \\}
        \\
        \\pub fn split(line: []const u8) ?usize {
        \\    var slash: ?usize = null;
        \\    for (0..line.len) |i| {
        \\        if (line[i] == '/') {
        \\            slash = i;
        \\        }
        \\    }
        \\    if (slash) |s| {
        \\        return s;
        \\    }
        \\    return null;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "pager port: value+pointer receivers, void mutator, #intCast, const-default slice fields" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // flibc's pager core, ported from hand-written Zig. The third pure port, and
    // it adds no new grammar — but it is the first struct to mix a value receiver
    // (`self Pager` -> `self: Pager`, the read-only queries) with a pointer
    // receiver (`self *mut Pager` -> `self: *Pager`, the scroll mutators), and the
    // first with a void-returning method (no return type -> `void`). The const-default
    // slice convention carries the field types: an immutable `[]u8` lowers to
    // `[]const u8`, while a mutable `[]mut u32` lowers to a bare `[]u32`. It also
    // reuses `#intCast`, a value `if`-expression, and Flash's mandatory braces on
    // the single-statement `break`. The full module lives in examples/pager.flash;
    // its core lowers token-identical to the reference.
    const got = try lowerSrc(a.allocator(),
        \\pub const Pager = struct {
        \\    text []u8,
        \\    lines []mut u32,
        \\    n usize,
        \\    top usize,
        \\    rows usize,
        \\
        \\    pub fn init(text []u8, slots []mut u32, rows usize) Pager {
        \\        var n usize = 0
        \\        if text.len > 0 && slots.len > 0 {
        \\            slots[0] = 0
        \\            n = 1
        \\            for i in 0..text.len {
        \\                if text[i] == '\n' && i + 1 < text.len {
        \\                    if n >= slots.len {
        \\                        break
        \\                    }
        \\                    slots[n] = #intCast(i + 1)
        \\                    n += 1
        \\                }
        \\            }
        \\        }
        \\        return .{ .text = text, .lines = slots, .n = n, .top = 0, .rows = rows }
        \\    }
        \\
        \\    pub fn maxTop(self Pager) usize {
        \\        return if (self.n > self.rows) self.n - self.rows else 0
        \\    }
        \\
        \\    pub fn down(self *mut Pager, k usize) {
        \\        const mt = self.maxTop()
        \\        self.top = if (self.top + k > mt) mt else self.top + k
        \\    }
        \\}
    );
    const want =
        \\pub const Pager = struct {
        \\    text: []const u8,
        \\    lines: []u32,
        \\    n: usize,
        \\    top: usize,
        \\    rows: usize,
        \\
        \\    pub fn init(text: []const u8, slots: []u32, rows: usize) Pager {
        \\        var n: usize = 0;
        \\        if (text.len > 0 and slots.len > 0) {
        \\            slots[0] = 0;
        \\            n = 1;
        \\            for (0..text.len) |i| {
        \\                if (text[i] == '\n' and i + 1 < text.len) {
        \\                    if (n >= slots.len) {
        \\                        break;
        \\                    }
        \\                    slots[n] = @intCast(i + 1);
        \\                    n += 1;
        \\                }
        \\            }
        \\        }
        \\        return .{ .text = text, .lines = slots, .n = n, .top = 0, .rows = rows };
        \\    }
        \\
        \\    pub fn maxTop(self: Pager) usize {
        \\        return if (self.n > self.rows) self.n - self.rows else 0;
        \\    }
        \\
        \\    pub fn down(self: *Pager, k: usize) void {
        \\        const mt = self.maxTop();
        \\        self.top = if (self.top + k > mt) mt else self.top + k;
        \\    }
        \\};
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "fsh port: `.?` optional unwrap — value-if fallback and pipe-stage name assert" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    // fsh's command-execution layer, ported from hand-written Zig. The leaf that
    // surfaces the `.?` optional-unwrap postfix: `cd` takes its argument or falls
    // back to "/" through a value-form `if` (`if (argc >= 2) argv[1].? else "/"`),
    // and a pipe stage asserts its command name is present before exec
    // (`left[0].?`). The argv vector keeps its `*mut [N]?[*:0]u8` shape, the
    // recast pipe view its `[*]const ?[*:0]const u8`, each spelled as the
    // const-default convention maps it. The full module lives in
    // examples/fsh.flash; its core lowers token-identical to the reference.
    const got = try lowerSrc(a.allocator(),
        \\use flibc
        \\
        \\fn cd(argv *mut [16]?[*:0]mut u8, argc usize) {
        \\    const target [*:0]u8 = if (argc >= 2) argv[1].? else "/"
        \\    _ = flibc.chdir(target)
        \\}
        \\
        \\fn pipeLeft(argv *mut [16]?[*:0]mut u8) {
        \\    const left [*]?[*:0]u8 = #ptrCast(argv)
        \\    _ = flibc.execvp(left[0].?, left)
        \\}
    );
    const want =
        \\const flibc = @import("flibc");
        \\
        \\fn cd(argv: *[16]?[*:0]u8, argc: usize) void {
        \\    const target: [*:0]const u8 = if (argc >= 2) argv[1].? else "/";
        \\    _ = flibc.chdir(target);
        \\}
        \\
        \\fn pipeLeft(argv: *[16]?[*:0]u8) void {
        \\    const left: [*]const ?[*:0]const u8 = @ptrCast(argv);
        \\    _ = flibc.execvp(left[0].?, left);
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "inline assembly: operands, positional sections, and the volatile modifier lower byte-for-byte" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn spin() {
        \\    asm volatile ("wfe")
        \\}
        \\
        \\fn read_iar() u64 {
        \\    var iar u64 = undefined
        \\    asm volatile ("mrs %[iar], S3_0_C12_C12_0"
        \\        : [iar] "=r" (iar),
        \\    )
        \\    return iar
        \\}
        \\
        \\fn eoi(iar u64) {
        \\    asm volatile ("msr S3_0_C12_C12_1, %[iar]"
        \\        :
        \\        : [iar] "r" (iar),
        \\    )
        \\}
        \\
        \\fn exec_path(path usize, argv usize) i32 {
        \\    return asm volatile ("svc #0"
        \\        : [ret] "={x0}" (-> i32),
        \\        : [nr] "{x8}" (11),
        \\          [path] "{x0}" (path),
        \\          [argv] "{x1}" (argv),
        \\        : .{ .memory = true })
        \\}
    );
    // The bare form stays single-line; any output/input operand breaks the
    // expression across lines with one positional colon per section. An empty
    // earlier section still occupies its `:` line (`eoi`), the trailing clobber
    // hugs the `)`, and an output/input last section closes the `)` on its own
    // line at the statement's depth (`read_iar`). Verified ast-check-clean and
    // fmt-idempotent against the FlashOS syscall/GIC asm corpus.
    const want =
        \\fn spin() void {
        \\    asm volatile ("wfe");
        \\}
        \\
        \\fn read_iar() u64 {
        \\    var iar: u64 = undefined;
        \\    asm volatile ("mrs %[iar], S3_0_C12_C12_0"
        \\        : [iar] "=r" (iar),
        \\    );
        \\    return iar;
        \\}
        \\
        \\fn eoi(iar: u64) void {
        \\    asm volatile ("msr S3_0_C12_C12_1, %[iar]"
        \\        :
        \\        : [iar] "r" (iar),
        \\    );
        \\}
        \\
        \\fn exec_path(path: usize, argv: usize) i32 {
        \\    return asm volatile ("svc #0"
        \\        : [ret] "={x0}" (-> i32),
        \\        : [nr] "{x8}" (11),
        \\          [path] "{x0}" (path),
        \\          [argv] "{x1}" (argv),
        \\        : .{ .memory = true });
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "inline assembly: multiline template, clobber-only single line, non-volatile, and a `#` intrinsic input" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\export fn user_entry() {
        \\    asm volatile (
        \\        \\bl pid1_main
        \\        \\mov x8, #2
        \\        \\svc #0
        \\    )
        \\}
        \\
        \\fn flush(addr u64) {
        \\    asm volatile (
        \\        \\dc cvau, %[a]
        \\        \\dsb ish
        \\        \\isb
        \\        :
        \\        : [a] "r" (addr),
        \\        : .{ .memory = true })
        \\}
        \\
        \\fn barrier() {
        \\    asm volatile ("dsb sy" ::: .{ .memory = true })
        \\}
        \\
        \\fn one() i32 {
        \\    return asm ("mov %[r], #1"
        \\        : [r] "=r" (-> i32),
        \\    )
        \\}
        \\
        \\fn set_pri() {
        \\    asm volatile ("msr S3_0_C4_C6_0, %[v]"
        \\        :
        \\        : [v] "r" (#as(u64, 255)),
        \\    )
        \\}
    );
    // A `\\` multiline template heads its own line(s) and forces the multi-line
    // layout even with no operands (`user_entry`). With no output and no input
    // operand and a single-string template, the expression stays on one line —
    // bare, or clobber-only with three tight colons (`barrier`). `asm` without
    // `volatile` keeps the space before `(` (`one`). A `#`-intrinsic input
    // lowers through the ordinary expression path (`#as` → `@as`).
    const want =
        \\export fn user_entry() callconv(.c) void {
        \\    asm volatile (
        \\        \\bl pid1_main
        \\        \\mov x8, #2
        \\        \\svc #0
        \\    );
        \\}
        \\
        \\fn flush(addr: u64) void {
        \\    asm volatile (
        \\        \\dc cvau, %[a]
        \\        \\dsb ish
        \\        \\isb
        \\        :
        \\        : [a] "r" (addr),
        \\        : .{ .memory = true });
        \\}
        \\
        \\fn barrier() void {
        \\    asm volatile ("dsb sy" ::: .{ .memory = true });
        \\}
        \\
        \\fn one() i32 {
        \\    return asm ("mov %[r], #1"
        \\        : [r] "=r" (-> i32),
        \\    );
        \\}
        \\
        \\fn set_pri() void {
        \\    asm volatile ("msr S3_0_C4_C6_0, %[v]"
        \\        :
        \\        : [v] "r" (@as(u64, 255)),
        \\    );
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "float literals lower verbatim — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const pi = 3.14
        \\const grav = 9.81e-2
        \\const tiny = 1_000.5e+0
    );
    // Float literals pass through unchanged: zig fmt emits them verbatim, and
    // Zig's grammar accepts the same decimal-float forms Flash defines. The
    // `_` digit separator and the signed exponent are both valid Zig syntax.
    const want =
        \\const pi = 3.14;
        \\
        \\const grav = 9.81e-2;
        \\
        \\const tiny = 1_000.5e+0;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "reserved value keywords lower verbatim — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const yes = true
        \\const no = false
        \\const nothing = null
        \\fn halt() noreturn {
        \\    unreachable
        \\}
        \\var seed u32 = undefined
    );
    // `true`/`false`/`null`/`undefined`/`unreachable` are reserved value words in
    // Flash and spelled identically in Zig, so they pass straight through: the
    // emitted text is byte-for-byte what zig fmt produces. Reserving them changed
    // how they parse (a value_word leaf, not an ident), not how they lower.
    const want =
        \\const yes = true;
        \\
        \\const no = false;
        \\
        \\const nothing = null;
        \\
        \\fn halt() noreturn {
        \\    unreachable;
        \\}
        \\
        \\var seed: u32 = undefined;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "the argv/cstr builtin type aliases yield to a same-named top-level declaration" {
    // Unshadowed, the aliases expand to the two pointer types the corpus relies
    // on but the surface gives no syntax for.
    {
        var a = std.heap.ArenaAllocator.init(testing.allocator);
        defer a.deinit();
        const got = try lowerSrc(a.allocator(),
            \\const a cstr = x
            \\const v argv = y
        );
        const want =
            \\const a: [*:0]const u8 = x;
            \\
            \\const v: [*]const ?[*:0]const u8 = y;
            \\
        ;
        try testing.expectEqualStrings(want, got);
    }
    // When the program declares a top-level `cstr` / `argv` constant, that
    // declaration wins: the builtin rewrite is suppressed and the name lowers
    // verbatim, so a user (or a future standard library) alias is no longer
    // silently overridden. The corpus declares neither, so no emitted byte moves.
    {
        var a = std.heap.ArenaAllocator.init(testing.allocator);
        defer a.deinit();
        const got = try lowerSrc(a.allocator(),
            \\const cstr = u8
            \\const argv = u32
            \\const a cstr = x
            \\const v argv = y
        );
        const want =
            \\const cstr = u8;
            \\
            \\const argv = u32;
            \\
            \\const a: cstr = x;
            \\
            \\const v: argv = y;
            \\
        ;
        try testing.expectEqualStrings(want, got);
    }
}

test "wrapping operators `-%` / `*%` lower verbatim — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const d = x -% y
        \\const p = x *% y
        \\const m = a -% b *% c
    );
    // Zig spells the wrapping operators identically and gives them the same
    // precedence, so they pass straight through: `*%` binds tighter than `-%`,
    // so `a -% b *% c` needs no parentheses and is byte-for-byte what zig fmt
    // emits. (`+%` already round-tripped this way.)
    const want =
        \\const d = x -% y;
        \\
        \\const p = x *% y;
        \\
        \\const m = a -% b *% c;
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "composite-type aliases lower to Zig type aliases — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const F = *fn(u8) u8
        \\const O = ?u8
        \\const S = []u8
        \\const M = *mut fn() void
        \\fn take(g Get([]u8)) void {
        \\    _ = g
        \\}
    );
    // A `?`/`*`/`[`/`fn`-led composite type is an expression (a `type_lit`), so
    // a type alias needs no wrapper: the alias value lowers through emitType
    // exactly as the same type in annotation position — `*fn` gains the const
    // pointee (`*const fn (u8) u8`, with zig fmt's anonymous-fn space), `[]u8`
    // the const element. A composite generic argument (`Get([]u8)`) rides the
    // same node. This output is byte-identical to `zig fmt`.
    const want =
        \\const F = *const fn (u8) u8;
        \\
        \\const O = ?u8;
        \\
        \\const S = []const u8;
        \\
        \\const M = *fn () void;
        \\
        \\fn take(g: Get([]const u8)) void {
        \\    _ = g;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "defer/errdefer block form lowers to a Zig brace body — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn run(fd i32) !void {
        \\    defer {
        \\        close(fd)
        \\        close(fd + 1)
        \\    }
        \\    errdefer {
        \\        close(0)
        \\    }
        \\    defer close(fd)
        \\    return
        \\}
    );
    // The block body renders like any brace body — statements indented one
    // level, the closing `}` without a `;` — and the single-statement form is
    // untouched. This output is byte-identical to `zig fmt`.
    const want =
        \\fn run(fd: i32) !void {
        \\    defer {
        \\        close(fd);
        \\        close(fd + 1);
        \\    }
        \\    errdefer {
        \\        close(0);
        \\    }
        \\    defer close(fd);
        \\    return;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "test blocks lower to Zig test blocks — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\use std
        \\
        \\fn add(a i32, b i32) i32 {
        \\    return a + b
        \\}
        \\
        \\test "add sums two integers" {
        \\    try std.testing.expectEqual(5, add(2, 3))
        \\}
        \\
        \\test "empty body is accepted" {}
    );
    // A test block is its own unit (one blank line on each side) and lowers
    // one-to-one: the quoted name verbatim, the body as a brace body, an empty
    // body collapsed to `{}`. This output is byte-identical to `zig fmt`.
    const want =
        \\const std = @import("std");
        \\
        \\fn add(a: i32, b: i32) i32 {
        \\    return a + b;
        \\}
        \\
        \\test "add sums two integers" {
        \\    try std.testing.expectEqual(5, add(2, 3));
        \\}
        \\
        \\test "empty body is accepted" {}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "loop else arms and the if else-capture lower — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn f(xs []u8, c bool) void {
        \\    if next() |v| {
        \\        consume(v)
        \\    } else |err| {
        \\        log(err)
        \\    }
        \\    while next() |v| {
        \\        consume(v)
        \\    } else |err| {
        \\        log(err)
        \\    }
        \\    while c {
        \\        step()
        \\    } else {
        \\        done()
        \\    }
        \\    for x in xs {
        \\        consume(x)
        \\    } else {
        \\        done()
        \\    }
        \\    if c {} else {}
        \\}
    );
    // Each else arm rides on the closing brace (`} else {`), the error capture
    // as `else |err|`; an empty arm collapses to `{}`. This output is
    // byte-identical to `zig fmt`.
    const want =
        \\fn f(xs: []const u8, c: bool) void {
        \\    if (next()) |v| {
        \\        consume(v);
        \\    } else |err| {
        \\        log(err);
        \\    }
        \\    while (next()) |v| {
        \\        consume(v);
        \\    } else |err| {
        \\        log(err);
        \\    }
        \\    while (c) {
        \\        step();
        \\    } else {
        \\        done();
        \\    }
        \\    for (xs) |x| {
        \\        consume(x);
        \\    } else {
        \\        done();
        \\    }
        \\    if (c) {} else {}
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "tuple types and multi-return lower to Zig tuples — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\const Pair = (u8, bool)
        \\fn pair() (u8, bool) {
        \\    return 42, true
        \\}
        \\fn lit() Pair {
        \\    return .{ 7, false }
        \\}
        \\fn first(t (u8, (u8, bool))) u8 {
        \\    return t[0] + t[1][0]
        \\}
        \\fn three() (u8, u8, u8) {
        \\    return 1, 2, 3
        \\}
    );
    // A tuple type lowers to Zig's inline positional struct (`struct { A, B }`,
    // zig fmt's one-line layout) in every position — alias value, return,
    // parameter, nested element. The multi-return value list folds into one
    // anonymous tuple literal (`return .{ 42, true };`); a written `.{ … }`
    // return and the postfix tuple index lower verbatim. This output is
    // byte-identical to `zig fmt`.
    const want =
        \\const Pair = struct { u8, bool };
        \\
        \\fn pair() struct { u8, bool } {
        \\    return .{ 42, true };
        \\}
        \\
        \\fn lit() Pair {
        \\    return .{ 7, false };
        \\}
        \\
        \\fn first(t: struct { u8, struct { u8, bool } }) u8 {
        \\    return t[0] + t[1][0];
        \\}
        \\
        \\fn three() struct { u8, u8, u8 } {
        \\    return .{ 1, 2, 3 };
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}

test "destructuring binds and assigns lower to Zig destructures — byte-identical to zig fmt" {
    var a = std.heap.ArenaAllocator.init(testing.allocator);
    defer a.deinit();
    const got = try lowerSrc(a.allocator(),
        \\fn pair() (u8, bool) {
        \\    return 42, true
        \\}
        \\fn demo() void {
        \\    a, b := pair()
        \\    _ = a
        \\    _ = b
        \\    tok, _ := pair()
        \\    _ = tok
        \\    _, ok := pair()
        \\    _ = ok
        \\    var x, y = pair()
        \\    x, y = pair()
        \\    var arr [3]u8 = .{ 0, 0, 0 }
        \\    arr[0], y = pair()
        \\    _ = x
        \\}
    );
    // The binding keyword repeats per name (Zig's native destructure
    // spelling); a `_` skip stays `_`; the assignment list lowers verbatim.
    // This output is byte-identical to `zig fmt`.
    const want =
        \\fn pair() struct { u8, bool } {
        \\    return .{ 42, true };
        \\}
        \\
        \\fn demo() void {
        \\    const a, const b = pair();
        \\    _ = a;
        \\    _ = b;
        \\    const tok, _ = pair();
        \\    _ = tok;
        \\    _, const ok = pair();
        \\    _ = ok;
        \\    var x, var y = pair();
        \\    x, y = pair();
        \\    var arr: [3]u8 = .{ 0, 0, 0 };
        \\    arr[0], y = pair();
        \\    _ = x;
        \\}
        \\
    ;
    try testing.expectEqualStrings(want, got);
}