ajhahn.de
← Flash
Flash 1985 lines
// Flash formatter — AST back to canonical Flash source text.
//
// `flashc fmt` is gofmt / zig fmt for Flash: it parses a `.flash` file and
// re-emits it in one canonical layout. This module is the renderer — the
// inverse of the lowering. Where lowering walks the AST to *Zig* text, the
// formatter walks the same AST to *Flash* text, so the two are mirror images
// and the emitter inventory lines up one-to-one (emitType / emitExpr /
// emitStmt / emitFn / …). The canonical layout is the lowering's layout rules
// transposed to Flash spelling: 4-space indent, one blank line between
// top-level units, the same brace-spacing zig fmt uses, mandatory braces.
//
// The Flash spelling is the lowering mapping read in reverse — the implicit
// `const` pointee that lowering makes explicit is dropped again here: `use X`
// stays a use line (not an import constant), `link "M"` lines stay as written
// (not folded into a comptime block), builtins keep the '#' sigil, parameters
// spell `name type` with no colon, the pointer families drop the const the
// lowering adds (`[]T`, `*T`, `[*]T` — `mut` opts back in), `&&`/`||` keep
// their Flash spelling, an untyped immutable binding renders in the `:=`
// short-declaration canon, and statement conditions carry no parentheses
// (`if c { … }`, `while c { … }`, `for x in xs { … }`).
//
// A value `if` is the one conditional that keeps its parentheses
// (`if (c) a else b`), matching the surface grammar. Statements carry no
// trailing semicolon. Source blank lines between statements are preserved
// (collapsed to one); top-level blank-line grouping is the author's.
//
// Three guarantees back every reformat, each gated by the test suite: a parse
// error refuses the file untouched (a formatter never destroys code); every
// comment in the input appears exactly once in the output; and formatting
// never changes the emitted Zig — lower(parse(src)) equals
// lower(parse(fmt(src))) byte for byte — so a reformat can never alter a
// program's meaning. The formatter is also idempotent: fmt(fmt(src)) == fmt(src).

use "ast"
use "token"
use "parser"
use "lexer"
use "lower"
use "support" as sup

// Re-exported for the integration suite: the lexer the formatter is built on,
// so a test can tokenize a formatted result to compare comment multisets
// without placing the lexer module in two module graphs at once.
pub const Lexer = lexer.Lexer

pub const Error = error{OutOfMemory}

// Format `src` to canonical Flash text. Runs its own parser; a parse error
// propagates as parser.Error.UnexpectedToken (the caller reads the parser's
// diagnostic and leaves the file untouched). The returned slice is arena-owned.
pub fn format(arena sup.Allocator, src []u8) parser.Error![]u8 {
    var p = parser.Parser.init(arena, src)
    program := try p.parseProgram()
    return render(arena, program, p.comments, src)
}

// Render an already-parsed program. `comments` is the source-ordered line
// comments the parser collected aside (the formatter reattaches them); `src`
// is the original buffer, used to recover blank-line and comment positions
// from the AST's source slices.
pub fn render(arena sup.Allocator, program ast.Program, comments []token.Token, src []u8) Error![]u8 {
    var p Printer = .{ .arena = arena, .src = src, .comments = comments }
    items := program.items
    var first = true
    for item, idx in items {
        // Standalone comments before this item — the file header before the
        // first item, a comment block before a declaration.
        lead := p.anchorOffset(itemLeadAnchor(item))
        if lead |off| {
            flushed := try p.flushStandalone(off, 0, first)
            if flushed {
                first = false
            }
        }
        // Blank lines between top-level items are PRESERVED, not imposed: a
        // source blank before the item (or its lead-in comment) renders as one
        // blank, and the author's tight grouping of consecutive declarations
        // (a run of `use`, a block of `pub const` re-exports) is kept tight.
        // The lowering's own "one blank between units" rule is for generated
        // Zig; the formatter keeps what the author wrote. A `comptime { … }`
        // block's only stored slice is its first statement, one line below the
        // `comptime {` head, so the blank check steps up to the head line.
        if !first {
            var blank_anchor ?usize = lead
            if item == .comptime_block {
                if lead |fs| {
                    blank_anchor = prevLineBreakOffset(p.src, fs)
                }
            }
            if blank_anchor |off| {
                if blankBeforeOffset(p.src, off) {
                    try p.raw("\n")
                }
            }
        }
        first = false
        p.boundary = p.nextOffset(items, idx + 1)
        switch item {
            .use_decl => |u| try p.emitUseDeclAt(u),
            // The lowering folds a `link` run into one comptime block; the
            // formatter keeps the `link "M"` lines the author wrote.
            .link_decl => |l| {
                try p.raw("link \"")
                try p.raw(l.module)
                try p.raw("\"")
            },
            .const_decl => |c| try p.emitConstDecl(c),
            .fn_decl => |f| try p.emitFn(f),
            .comptime_block => |stmts| {
                try p.raw("comptime ")
                try p.emitBlockBody(stmts, 0)
            },
            .test_decl => |t| {
                try p.raw("test ")
                try p.raw(t.name)
                try p.raw(" ")
                try p.emitBlockBody(t.body, 0)
            },
        }
        try p.flushTrailing(p.anchorOffset(itemTailAnchor(item)))
        try p.raw("\n")
    }
    // End of file: emit every comment that has not been placed yet, at depth 0
    // (a file-tail comment, or one a placement heuristic could not site earlier).
    _ = try p.flushStandalone(p.src.len, 0, first)
    return p.buf.toOwnedSlice(arena)
}

const Printer = struct {
    arena sup.Allocator,
    src []u8,
    buf sup.List(u8) = .empty,
    // The source-ordered line comments to reattach while walking the AST, and a
    // cursor into them. The walk merges the two streams: at each element it
    // flushes the comments that precede it (standalone, on their own lines) and
    // appends a same-line one as a trailing comment. Every comment is emitted
    // exactly once — anything not placed earlier is flushed at end of file.
    comments []token.Token,
    c_idx usize = 0,
    // The source offset just past the construct currently being emitted — the
    // exclusive upper bound for a block-close comment flush, so a block never
    // adopts comments that belong to a later sibling. Set by each sequence loop
    // (items, statements) to the next element's offset; saved and restored
    // around nested blocks.
    boundary usize = 0,

    fn raw(self *mut Printer, s []u8) Error!void {
        try self.buf.appendSlice(self.arena, s)
    }

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

    // --- comment plumbing ------------------------------------------------

    // The byte offset of an AST source slice into `src`, or null when the slice
    // is empty or not a view into the buffer (a defensive guard — every AST
    // string is meant to be a real source slice).
    fn anchorOffset(self *mut Printer, slice ?[]u8) ?usize {
        a := slice orelse return null
        if a.len == 0 {
            return null
        }
        base := #intFromPtr(self.src.ptr)
        ap := #intFromPtr(a.ptr)
        if ap < base || ap >= base + self.src.len {
            return null
        }
        return ap - base
    }

    // The lead-anchor offset of items[idx], or end of file when idx is past the
    // last item — the boundary for the preceding item's block-close flush.
    fn nextOffset(self *mut Printer, items []ast.Item, idx usize) usize {
        if idx >= items.len {
            return self.src.len
        }
        return self.anchorOffset(itemLeadAnchor(items[idx])) orelse self.src.len
    }

    // Emit every pending standalone comment whose start is before `limit`, each
    // on its own line at `depth`. A source blank line before a comment is
    // preserved (collapsed to one), except before the first emitted line when
    // `suppress_leading_blank`. Returns whether any comment was emitted.
    fn flushStandalone(self *mut Printer, limit usize, depth usize, suppress_leading_blank bool) Error!bool {
        var emitted = false
        while self.c_idx < self.comments.len {
            c := self.comments[self.c_idx]
            if c.start >= limit {
                break
            }
            if emitted || !suppress_leading_blank {
                if blankBeforeOffset(self.src, c.start) {
                    try self.raw("\n")
                }
            }
            try self.indent(depth)
            try self.raw(c.lexeme(self.src))
            try self.raw("\n")
            emitted = true
            self.c_idx += 1
        }
        return emitted
    }

    // If the next pending comment is a trailing comment on the same source line
    // as the element anchored at `anchor` (no newline between), append it to the
    // current output line as ` <lexeme>` and consume it.
    fn flushTrailing(self *mut Printer, anchor ?usize) Error!void {
        off := anchor orelse return
        if self.c_idx >= self.comments.len {
            return
        }
        c := self.comments[self.c_idx]
        if commentIsTrailing(self.src, c.start) && noNewlineBetween(self.src, off, c.start) {
            try self.raw(" ")
            try self.raw(c.lexeme(self.src))
            self.c_idx += 1
        }
    }

    // At a block's closing brace (its statements were at `inner_depth`), flush
    // the pending comments that belong inside — those before `boundary` (the
    // next sibling after the block) AND indented past the block's owner. The
    // offset bound stops a block from adopting a later sibling's comments; the
    // relative-column rule keeps a comment that lines up with the owner outside.
    // Together they site a block-final comment without the brace's own offset,
    // which the tree does not carry.
    fn flushBlockClose(self *mut Printer, inner_depth usize, boundary usize) Error!void {
        var threshold usize = 0
        if inner_depth != 0 {
            threshold = (inner_depth - 1) * 4
        }
        while self.c_idx < self.comments.len {
            c := self.comments[self.c_idx]
            if c.start >= boundary {
                break
            }
            if commentColumn(self.src, c.start) <= threshold {
                break
            }
            if blankBeforeOffset(self.src, c.start) {
                try self.raw("\n")
            }
            try self.indent(inner_depth)
            try self.raw(c.lexeme(self.src))
            try self.raw("\n")
            self.c_idx += 1
        }
    }

    // --- items -----------------------------------------------------------

    // A top-level function: doc block, then the signature and body. The caller
    // (render) flushes a trailing comment and emits the line break, so a
    // same-line comment on a one-line declaration can still attach.
    fn emitFn(self *mut Printer, f ast.FnDecl) Error!void {
        try self.emitDoc(f.doc, 0)
        try self.emitFnAt(f, 0)
    }

    // Emit a function whose signature starts at the current column and whose
    // closing brace returns to `depth`. Flash spells parameters `name type`
    // (no colon), drops the `->` before the return type, and omits the return
    // entirely when absent (the lowering's `void` is implicit). A bodyless
    // `extern` prototype simply ends — Flash has no terminating `;`.
    fn emitFnAt(self *mut Printer, 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.raw("fn ")
        try self.raw(f.name)
        try self.raw("(")
        for prm, idx in f.params {
            if idx != 0 {
                try self.raw(", ")
            }
            if prm.is_comptime {
                try self.raw("comptime ")
            }
            try self.raw(prm.name orelse "_")
            try self.raw(" ")
            try self.emitType(prm.type)
        }
        try self.raw(")")
        // `linksection(…)` precedes any `callconv(…)`, Zig's slot order.
        if f.link_section |ls| {
            try self.raw(" linksection(")
            try self.emitExpr(ls)
            try self.raw(")")
        }
        // An explicit `callconv(…)` sits between the parameter list and the
        // return type. The formatter emits it only when the source wrote one —
        // the implicit C ABI of a bare `export fn` is the lowering's to add, so
        // re-emitting it here would invent surface the author did not write.
        if f.call_conv |cc| {
            try self.raw(" callconv(")
            try self.emitExpr(cc)
            try self.raw(")")
        }
        // The return type follows directly, on the same physical line as the
        // `)` (which is how the parser knows it is a return, not the next item).
        if f.ret |r| {
            try self.raw(" ")
            try self.emitType(r)
        }
        if f.body |body| {
            try self.raw(" ")
            try self.emitBlockBody(body, depth)
        }
    }

    // A top-level constant: doc block, then the declaration. The caller (render)
    // flushes a trailing comment and emits the line break.
    fn emitConstDecl(self *mut Printer, c ast.ConstDecl) Error!void {
        try self.emitDoc(c.doc, 0)
        try self.emitConstDeclAt(c, 0)
    }

    // Emit `[pub ][export |extern ](const|var) NAME[ T][ align(A)][ linksection(S)][ = value]` at the current
    // column, ending at the value (or the type, for a valueless `extern var`)
    // with no trailing newline. `depth` threads into the value so a
    // multiline string or a nested type definition lays out one level deeper. A
    // top-level constant is never rewritten to `:=` (the short declaration is
    // statement-only grammar).
    fn emitConstDeclAt(self *mut Printer, c ast.ConstDecl, depth usize) Error!void {
        if c.is_pub {
            try self.raw("pub ")
        }
        if c.is_export {
            try self.raw("export ")
        }
        if c.is_extern {
            try self.raw("extern ")
        }
        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)
        }
        // `align(…)` then `linksection(…)` sit between the type and `=`,
        // Zig's slot order.
        if c.align_expr |ae| {
            try self.raw(" align(")
            try self.emitExpr(ae)
            try self.raw(")")
        }
        if c.link_section |ls| {
            try self.raw(" linksection(")
            try self.emitExpr(ls)
            try self.raw(")")
        }
        // An `extern var` is valueless — the declaration ends at the type.
        if c.value |value| {
            if value == .multiline_str {
                try self.raw(" ")
                try self.emitMultilineRhs(value.multiline_str, depth)
            } else {
                try self.raw(" = ")
                try self.emitValue(value, depth)
            }
        }
    }

    // Emit one import as `[pub ]use TARGET[ as ALIAS]`. A quoted file import
    // names the module stem in quotes (`use "syscalls" as sys`); a bare module
    // import names it unquoted (`use flibc`). The same form serves at the top
    // level and inside a struct body.
    fn emitUseDeclAt(self *mut Printer, u ast.UseDecl) Error!void {
        if u.is_pub {
            try self.raw("pub ")
        }
        try self.raw("use ")
        if u.is_file {
            try self.raw("\"")
            try self.raw(u.module)
            try self.raw("\"")
        } else {
            try self.raw(u.module)
        }
        if u.alias |a| {
            try self.raw(" as ")
            try self.raw(a)
        }
    }

    // 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 the lowering minus its trailing `;`:
    // an `=` and a newline, then the `\\` lines at depth + 1, ending on the last
    // `\\` line with no trailing newline (the caller closes the statement).
    // `depth` is the statement's own indent.
    fn emitMultilineRhs(self *mut Printer, lines [][]u8, depth usize) Error!void {
        try self.raw("=\n")
        for ln, idx in lines {
            if idx != 0 {
                try self.raw("\n")
            }
            try self.indent(depth + 1)
            try self.raw("\\\\")
            try self.raw(ln)
        }
    }

    // Emit the value of a binding or constant. A struct/enum/union type
    // definition lays out across multiple lines with its closing brace at
    // `depth`; every other value is a single-line expression.
    fn emitValue(self *mut Printer, 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 { … }` / `union(…) { … }` definition.
    // Fields/variants sit one per line at `depth + 1` with a trailing comma; the
    // closing brace returns to `depth`. Flash spells a field `name type` (no
    // colon), exactly as a parameter; a union variant's payload likewise, a bare
    // name being a void variant. `sb` in each arm is the offset just past the
    // whole container, the bound for its block-close comment flushes.
    fn emitTypeDef(self *mut Printer, x ast.Expr, depth usize) Error!void {
        switch x {
            .struct_def => |sd| {
                // The layout modifier — `packed` / `extern` — prefixes the
                // keyword verbatim.
                if sd.layout |l| {
                    try self.raw(l)
                    try self.raw(" ")
                }
                try self.raw("struct {\n")
                sb := self.boundary
                var firstm = true
                for f in sd.fields {
                    lead := self.anchorOffset(if (f.doc.len > 0) f.doc[0] else f.name)
                    if lead |off| {
                        flushed := try self.flushStandalone(off, depth + 1, firstm)
                        if flushed {
                            firstm = false
                        }
                    }
                    firstm = false
                    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)
                    if f.default |d| {
                        try self.raw(" = ")
                        try self.emitExpr(d)
                    }
                    try self.raw(",")
                    try self.flushTrailing(self.anchorOffset(f.name))
                    try self.raw("\n")
                }
                try self.emitContainerDecls(sd.decls, sd.fields.len != 0, depth, sb)
                try self.flushBlockClose(depth + 1, sb)
                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(")")
                }
                try self.raw(" {\n")
                sb := self.boundary
                var firstm = true
                for v in ed.variants {
                    lead := self.anchorOffset(if (v.doc.len > 0) v.doc[0] else v.name)
                    if lead |off| {
                        flushed := try self.flushStandalone(off, depth + 1, firstm)
                        if flushed {
                            firstm = false
                        }
                    }
                    firstm = false
                    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(",")
                    try self.flushTrailing(self.anchorOffset(v.name))
                    try self.raw("\n")
                }
                try self.emitContainerDecls(ed.decls, ed.variants.len != 0, depth, sb)
                try self.flushBlockClose(depth + 1, sb)
                try self.indent(depth)
                try self.raw("}")
            },
            .union_def => |ud| {
                try self.raw("union")
                if ud.tag |t| {
                    try self.raw("(")
                    try self.raw(t)
                    try self.raw(")")
                }
                try self.raw(" {\n")
                sb := self.boundary
                var firstm = true
                for v in ud.variants {
                    lead := self.anchorOffset(if (v.doc.len > 0) v.doc[0] else v.name)
                    if lead |off| {
                        flushed := try self.flushStandalone(off, depth + 1, firstm)
                        if flushed {
                            firstm = false
                        }
                    }
                    firstm = false
                    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(",")
                    try self.flushTrailing(self.anchorOffset(v.name))
                    try self.raw("\n")
                }
                try self.emitContainerDecls(ud.decls, ud.variants.len != 0, depth, sb)
                try self.flushBlockClose(depth + 1, sb)
                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. A container whose first member is a
    // declaration gets no leading blank. `sb` is the offset just past the whole
    // container, restored as the boundary when the decls are done.
    fn emitContainerDecls(self *mut Printer, decls []ast.ContainerDecl, has_members bool, depth usize, sb usize) Error!void {
        for d, idx in decls {
            if idx != 0 || has_members {
                try self.raw("\n")
            }
            lead := self.anchorOffset(declLeadAnchor(d))
            if lead |off| {
                _ = try self.flushStandalone(off, depth + 1, true)
            }
            // The next declaration (or the container boundary) bounds this
            // one's method-body block-close flushes, so a method never
            // adopts a comment that belongs to a later method.
            if idx + 1 < decls.len {
                self.boundary = self.anchorOffset(declLeadAnchor(decls[idx + 1])) orelse sb
            } else {
                self.boundary = sb
            }
            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| {
                    try self.indent(depth + 1)
                    try self.emitUseDeclAt(u)
                },
            }
            try self.flushTrailing(self.anchorOffset(declTailAnchor(d)))
            try self.raw("\n")
        }
        self.boundary = sb
    }

    // --- statements ------------------------------------------------------

    // Emit a brace-delimited block body, opening at the current column. An empty
    // statement list collapses to `{}`; a non-empty one opens `{`, lays out one
    // statement per line at `depth + 1`, and closes `}` back at `depth`.
    fn emitBlockBody(self *mut Printer, stmts []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.
    // A source blank line between two statements is preserved (collapsed to a
    // single blank); there is never a blank after the opening `{` (the first
    // statement carries none) or before the closing `}`.
    fn emitBlock(self *mut Printer, stmts []ast.Stmt, depth usize) Error!void {
        bb := self.boundary // the offset just past this whole block
        var first = true
        for s, idx in stmts {
            aoff := self.anchorOffset(stmtAnchor(s))
            if aoff |off| {
                flushed := try self.flushStandalone(off, depth, first)
                if flushed {
                    first = false
                }
            }
            if !first {
                if aoff |off| {
                    if blankBeforeOffset(self.src, off) {
                        try self.raw("\n")
                    }
                }
            }
            first = false
            // The next statement (or the block boundary, for the last) bounds
            // this statement's own inner block-close flushes.
            if idx + 1 < stmts.len {
                self.boundary = self.anchorOffset(stmtAnchor(stmts[idx + 1])) orelse bb
            } else {
                self.boundary = bb
            }
            try self.indent(depth)
            try self.emitStmt(s, depth)
            try self.flushTrailing(aoff)
            try self.raw("\n")
        }
        self.boundary = bb
        // Comments between the last statement and the closing brace that are
        // indented past the block's owner belong inside; flush them here.
        try self.flushBlockClose(depth, bb)
    }

    // Emit a `///` doc-comment block: one line per entry at `depth`, the three
    // slashes plus the preserved content. An empty `doc` emits nothing.
    fn emitDoc(self *mut Printer, doc [][]u8, depth usize) Error!void {
        for line in doc {
            try self.indent(depth)
            try self.raw("///")
            try self.raw(line)
            try self.raw("\n")
        }
    }

    fn emitStmt(self *mut Printer, 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)
                }
            },
            // The short-declaration canon: an untyped, non-`align`,
            // non-`comptime` immutable binding renders `name := value`,
            // whatever spelling the author used. `name := e` and an untyped
            // `const name = e` lower identically, so this changes only the
            // surface form, never the meaning. Every other binding — `var`, a
            // typed or aligned `const`, a `comptime` local — keeps its keyword
            // form (`:=` has no typed, mutable, or comptime spelling).
            .bind => |b| {
                short := !b.is_mut && b.type == null && b.align_expr == null && !b.is_comptime
                if short {
                    try self.raw(b.name)
                    if b.value == .multiline_str {
                        try self.raw(" :")
                        try self.emitMultilineRhs(b.value.multiline_str, depth)
                    } else {
                        try self.raw(" := ")
                        try self.emitValue(b.value, depth)
                    }
                } else {
                    if b.is_comptime {
                        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)
                    }
                    if b.align_expr |ae| {
                        try self.raw(" align(")
                        try self.emitExpr(ae)
                        try self.raw(")")
                    }
                    if b.value == .multiline_str {
                        try self.raw(" ")
                        try self.emitMultilineRhs(b.value.multiline_str, depth)
                    } else {
                        try self.raw(" = ")
                        try self.emitValue(b.value, depth)
                    }
                }
            },
            // The op lexeme — "=", "+=", … — re-emits verbatim.
            .assign => |a| {
                try self.emitExprAt(a.target, depth)
                try self.raw(" ")
                try self.raw(a.op)
                try self.raw(" ")
                try self.emitExprAt(a.value, depth)
            },
            // The `:=` canon extends to destructures: an immutable one renders
            // `a, b := e` whether the author wrote that or `const a, b = e`
            // (a destructure has no type, `align`, or `comptime` spelling to
            // block the rewrite); a mutable one keeps `var a, b = e`.
            .destructure => |d| {
                if d.is_mut {
                    try self.raw("var ")
                }
                for maybe, i in d.names {
                    if i != 0 {
                        try self.raw(", ")
                    }
                    try self.raw(maybe orelse "_")
                }
                try self.raw(if (d.is_mut) " = " else " := ")
                try self.emitValue(d.value, depth)
            },
            .destructure_assign => |da| {
                for t, i in da.targets {
                    if i != 0 {
                        try self.raw(", ")
                    }
                    try self.emitExprAt(t, depth)
                }
                try self.raw(" = ")
                try self.emitExprAt(da.value, depth)
            },
            .if_stmt => |iff| try self.emitIf(iff, depth),
            .defer_stmt => |inner| {
                try self.raw("defer ")
                try self.emitStmt(inner.*, depth)
            },
            // The `|err|` capture re-renders tight (`errdefer |err| …`) — a
            // spaced `| err |` canonicalises.
            .errdefer_stmt => |ed| {
                try self.raw("errdefer ")
                if ed.capture |cap| {
                    try self.raw("|")
                    try self.raw(cap)
                    try self.raw("| ")
                }
                try self.emitStmt(ed.body.*, depth)
            },
            .defer_block => |stmts| {
                try self.raw("defer ")
                try self.emitBlockBody(stmts, depth)
            },
            .errdefer_block => |ed| {
                try self.raw("errdefer ")
                if ed.capture |cap| {
                    try self.raw("|")
                    try self.raw(cap)
                    try self.raw("| ")
                }
                try self.emitBlockBody(ed.body, depth)
            },
            // A loop label prefixes the whole loop — `outer: inline while`.
            .while_stmt => |w| {
                if w.label |l| {
                    try self.raw(l)
                    try self.raw(": ")
                }
                if w.is_inline {
                    try self.raw("inline ")
                }
                try self.raw("while ")
                try self.emitExprAt(w.cond, depth)
                if w.capture |cap| {
                    try self.raw(" |")
                    if w.capture_is_ptr {
                        try self.raw("*")
                    }
                    try self.raw(cap)
                    try self.raw("|")
                }
                try self.raw(" ")
                try self.emitLoopBody(w.body, w.else_body, w.else_capture, depth)
            },
            .for_stmt => |fr| {
                if fr.label |l| {
                    try self.raw(l)
                    try self.raw(": ")
                }
                if fr.is_inline {
                    try self.raw("inline ")
                }
                try self.raw("for ")
                for c, i in fr.captures {
                    if i != 0 {
                        try self.raw(", ")
                    }
                    if i == 0 && fr.elem_is_ptr {
                        try self.raw("*")
                    }
                    try self.raw(c)
                }
                try self.raw(" in ")
                try self.emitExprAt(fr.iter, depth)
                if fr.range_hi |hi| {
                    try self.raw("..")
                    try self.emitExprAt(hi, depth)
                }
                try self.raw(" ")
                try self.emitLoopBody(fr.body, fr.else_body, null, depth)
            },
            .expr => |x| try self.emitExprAt(x, depth),
        }
    }

    // `if cond { … }`, with an `else { … }` arm or, when the else body is exactly
    // one nested if, an idiomatic `else if … { … }` chain. The condition carries
    // no parentheses (the statement form).
    fn emitIf(self *mut Printer, iff ast.If, depth usize) Error!void {
        try self.raw("if ")
        try self.emitExprAt(iff.cond, depth)
        if iff.capture |cap| {
            try self.raw(" |")
            if iff.capture_is_ptr {
                try self.raw("*")
            }
            try self.raw(cap)
            try self.raw("|")
        }
        try self.raw(" ")
        after_if := self.boundary
        if iff.else_body |eb| {
            // The then-body's block-close is bounded by the else clause, so it
            // does not adopt comments that belong to the else arm.
            self.boundary = self.anchorOffset(elseAnchor(eb)) orelse after_if
            try self.emitBlockBody(iff.body, depth)
            self.boundary = after_if
            if eb.len == 1 && eb[0] == .if_stmt {
                try self.raw(" else ")
                try self.emitIf(eb[0].if_stmt, depth)
                // ` else { … }`, the error capture printed as ` else |err| { … }`.
            } else {
                try self.raw(" else ")
                if iff.else_capture |cap| {
                    try self.raw("|")
                    try self.raw(cap)
                    try self.raw("| ")
                }
                try self.emitBlockBody(eb, depth)
            }
        } else {
            try self.emitBlockBody(iff.body, depth)
        }
    }

    // A loop body with its optional `else` arm (`while`/`for … else`). Mirrors
    // emitIf's else handling: the body's block-close is bounded by the else
    // clause so it does not adopt the else arm's comments; the capture (the
    // `while` error binding) prints as ` else |err| { … }`.
    fn emitLoopBody(self *mut Printer, body []ast.Stmt, else_body ?[]mut ast.Stmt, else_capture ?[]u8, depth usize) Error!void {
        after_loop := self.boundary
        if else_body |eb| {
            self.boundary = self.anchorOffset(elseAnchor(eb)) orelse after_loop
            try self.emitBlockBody(body, depth)
            self.boundary = after_loop
            try self.raw(" else ")
            if else_capture |cap| {
                try self.raw("|")
                try self.raw(cap)
                try self.raw("| ")
            }
            try self.emitBlockBody(eb, depth)
        } else {
            try self.emitBlockBody(body, depth)
        }
    }

    // --- expressions -----------------------------------------------------

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

    // Emit an expression at indentation `depth`. Most forms are single-line and
    // thread `depth` unchanged; the multi-line forms — a labeled block and the
    // `switch` expression — lay their inner statements / prongs out at `depth + 1`
    // and close at `depth`.
    fn emitExprAt(self *mut Printer, x ast.Expr, depth usize) Error!void {
        switch x {
            .int, .float, .string, .char, .ident, .value_word => |s| try self.raw(s),
            // Reached only outside a const/binding/discard value (a call
            // argument, an asm template). Indentation before `\\` does not
            // affect the value; the byte-exact layout is guaranteed for the
            // routed value positions, not here (the same deliberate limit the
            // lowering carries).
            .multiline_str => |lines| {
                try self.raw("\n")
                for ln in lines {
                    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| {
                try self.emitExprAt(d.*, depth)
                try self.raw(".*")
            },
            .optional_unwrap => |u| {
                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 => |sl| {
                try self.emitExprAt(sl.base.*, depth)
                try self.raw("[")
                try self.emitExprAt(sl.lo.*, depth)
                spaced := sliceBoundSpaces(sl.lo.*) || (sl.hi != null && sliceBoundSpaces(sl.hi.?.*))
                if spaced {
                    try self.raw(" ")
                }
                try self.raw("..")
                if sl.hi |hi| {
                    if spaced {
                        try self.raw(" ")
                    }
                    try self.emitExprAt(hi.*, depth)
                }
                if sl.sentinel |sen| {
                    try self.raw(" :")
                    try self.emitExprAt(sen.*, depth)
                }
                try self.raw("]")
            },
            // The AST holds the bare intrinsic name; Flash spells it with the
            // '#' sigil (the lowering's '@' is the Tier-0 backend's).
            .builtin_call => |b| {
                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)
            },
            // The op lexeme re-emits verbatim — `&&` / `||` keep their Flash
            // spelling (their `and` / `or` translation is the lowering's).
            .binary => |b| {
                try self.emitExprAt(b.lhs.*, depth)
                try self.raw(" ")
                try self.raw(b.op)
                try self.raw(" ")
                try self.emitExprAt(b.rhs.*, depth)
            },
            .struct_lit => |fields| {
                spaced := !(fields.len == 0 || (fields.len == 1 && fields[0].name == null))
                try self.raw(if (spaced) ".{ " else ".{")
                for f, idx in fields {
                    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 "}")
            },
            .typed_lit => |tl| {
                try self.emitExprAt(tl.type.*, depth)
                spaced := !(tl.fields.len == 0 || (tl.fields.len == 1 && tl.fields[0].name == null))
                try self.raw(if (spaced) "{ " else "{")
                for f, idx in tl.fields {
                    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_lit => |t| try self.emitType(t.*),
            .enum_lit => |v| {
                try self.raw(".")
                try self.raw(v)
            },
            .error_lit => |n| {
                try self.raw("error.")
                try self.raw(n)
            },
            .error_set => |names| {
                spaced := names.len > 1
                try self.raw(if (spaced) "error{ " else "error{")
                for n, idx in names {
                    if idx != 0 {
                        try self.raw(", ")
                    }
                    try self.raw(n)
                }
                try self.raw(if (spaced) " }" else "}")
            },
            .struct_def, .enum_def, .union_def => try self.emitTypeDef(x, depth),
            .group => |g| {
                try self.raw("(")
                try self.emitExprAt(g.*, depth)
                try self.raw(")")
            },
            // A value `if` keeps its parentheses, the one conditional that does —
            // `if (cond) a else b`, exactly the surface grammar requires.
            .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 carries no parentheses (the
            // statement-header form); prongs lay out one per line at depth + 1.
            .switch_expr => |sw| {
                try self.raw("switch ")
                try self.emitExprAt(sw.subject.*, depth)
                try self.raw(" {\n")
                swb := self.boundary // the offset just past the whole switch
                var firstm = true
                for prong, pidx in sw.prongs {
                    var lead ?usize = null
                    if prong.patterns.len > 0 {
                        lead = self.anchorOffset(exprAnchor(prong.patterns[0].lo))
                    }
                    if lead |off| {
                        flushed := try self.flushStandalone(off, depth + 1, firstm)
                        if flushed {
                            firstm = false
                        }
                    }
                    firstm = false
                    // The next prong (or the switch boundary, for the last)
                    // bounds this prong's own inner block-close flushes, so a
                    // block-bodied prong never adopts a later prong's comments.
                    if pidx + 1 < sw.prongs.len {
                        self.boundary = self.anchorOffset(prongAnchor(sw.prongs[pidx + 1])) orelse swb
                    } else {
                        self.boundary = swb
                    }
                    try self.indent(depth + 1)
                    if prong.is_else {
                        try self.raw("else")
                    } else {
                        for pat, idx in prong.patterns {
                            if idx != 0 {
                                try self.raw(", ")
                            }
                            try self.emitExprAt(pat.lo, depth + 1)
                            if pat.hi |hi| {
                                try self.raw("...")
                                try self.emitExprAt(hi, depth + 1)
                            }
                        }
                    }
                    try self.raw(" => ")
                    if prong.capture |cap| {
                        try self.raw("|")
                        if prong.capture_is_ptr {
                            try self.raw("*")
                        }
                        try self.raw(cap)
                        try self.raw("| ")
                    }
                    try self.emitExprAt(prong.body, depth + 1)
                    try self.raw(",")
                    try self.flushTrailing(lead)
                    try self.raw("\n")
                }
                self.boundary = swb
                try self.flushBlockClose(depth + 1, swb)
                try self.indent(depth)
                try self.raw("}")
                // The value list re-emits as written: `return v` for one
                // value, `return a, b` for the multi-return sugar (a
                // written `return .{ a, b }` is ONE struct_lit value, so
                // each spelling round-trips to itself).
            },
            .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),
            .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 => |maybe| {
                try self.raw("continue")
                if maybe |l| {
                    try self.raw(" :")
                    try self.raw(l)
                }
            },
            .ret => |maybe| {
                try self.raw("return")
                if maybe |vals| {
                    try self.raw(" ")
                    for v, idx in vals {
                        if idx != 0 {
                            try self.raw(", ")
                        }
                        try self.emitExprAt(v, depth)
                    }
                }
            },
        }
    }

    fn emitArgs(self *mut Printer, args []mut ast.Expr, depth usize) Error!void {
        try self.raw("(")
        for a, idx in args {
            if idx != 0 {
                try self.raw(", ")
            }
            try self.emitExprAt(a, depth)
        }
        try self.raw(")")
    }

    // --- types -----------------------------------------------------------

    // The Flash spelling of a type — the lowering's mapping in reverse. The
    // const-pointee default is implicit, so the pointer families drop the
    // explicit `const` the lowering adds (`[]T`, `*T`, `[*]T`), and `mut` opts a
    // pointee back into mutability. `argv` / `cstr` are ordinary names here: the
    // builtin-alias expansion is the lowering's, not the surface's.
    // The optional `align(expr)` qualifier inside a pointer/slice type —
    // canonical form: tight after the prefix, one space after the `)`, before
    // any `mut`/`volatile` (`[]align(16) mut u8`). Nothing when absent; a
    // spaced source `align ( 16 )` canonicalises here.
    fn emitTypeAlign(self *mut Printer, align_expr ?*mut ast.Expr) Error!void {
        if align_expr |ae| {
            try self.raw("align(")
            try self.emitExpr(ae.*)
            try self.raw(") ")
        }
    }

    fn emitType(self *mut Printer, t ast.TypeRef) Error!void {
        switch t {
            .name => |n| try self.raw(n),
            .slice => |p| {
                try self.raw("[]")
                try self.emitTypeAlign(p.align_expr)
                try self.emitType(p.elem.*)
            },
            .slice_mut => |p| {
                try self.raw("[]")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("mut ")
                try self.emitType(p.elem.*)
            },
            .slice_sentinel => |sp| {
                try self.raw("[:")
                try self.emitExpr(sp.sentinel.*)
                try self.raw("]")
                try self.emitTypeAlign(sp.align_expr)
                try self.emitType(sp.elem.*)
            },
            .slice_sentinel_mut => |sp| {
                try self.raw("[:")
                try self.emitExpr(sp.sentinel.*)
                try self.raw("]")
                try self.emitTypeAlign(sp.align_expr)
                try self.raw("mut ")
                try self.emitType(sp.elem.*)
            },
            .many_ptr => |p| {
                try self.raw("[*]")
                try self.emitTypeAlign(p.align_expr)
                try self.emitType(p.elem.*)
            },
            .many_ptr_mut => |p| {
                try self.raw("[*]")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("mut ")
                try self.emitType(p.elem.*)
            },
            .many_ptr_volatile => |p| {
                try self.raw("[*]")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("volatile ")
                try self.emitType(p.elem.*)
            },
            .many_ptr_mut_volatile => |p| {
                try self.raw("[*]")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("mut volatile ")
                try self.emitType(p.elem.*)
            },
            .many_ptr_sentinel => |sp| {
                try self.raw("[*:")
                try self.emitExpr(sp.sentinel.*)
                try self.raw("]")
                try self.emitTypeAlign(sp.align_expr)
                try self.emitType(sp.elem.*)
            },
            .many_ptr_sentinel_mut => |sp| {
                try self.raw("[*:")
                try self.emitExpr(sp.sentinel.*)
                try self.raw("]")
                try self.emitTypeAlign(sp.align_expr)
                try self.raw("mut ")
                try self.emitType(sp.elem.*)
            },
            .ptr => |p| {
                try self.raw("*")
                try self.emitTypeAlign(p.align_expr)
                try self.emitType(p.elem.*)
            },
            .ptr_mut => |p| {
                try self.raw("*")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("mut ")
                try self.emitType(p.elem.*)
            },
            .ptr_volatile => |p| {
                try self.raw("*")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("volatile ")
                try self.emitType(p.elem.*)
            },
            .ptr_mut_volatile => |p| {
                try self.raw("*")
                try self.emitTypeAlign(p.align_expr)
                try self.raw("mut volatile ")
                try self.emitType(p.elem.*)
            },
            .array => |arr| {
                try self.raw("[")
                try self.emitExpr(arr.len.*)
                try self.raw("]")
                try self.emitType(arr.elem.*)
            },
            .array_sentinel => |a| {
                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| {
                try self.raw("[_]")
                try self.emitType(elem.*)
            },
            .array_inferred_sentinel => |sp| {
                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| {
                if eu.set |st| {
                    try self.emitType(st.*)
                }
                try self.raw("!")
                try self.emitType(eu.payload.*)
            },
            // `fn(P, …) R` — Flash writes the parameter list tight after `fn`
            // (no space), and omits the return when absent.
            .fn_type => |ft| {
                try self.raw("fn(")
                for p, idx in ft.params {
                    if idx != 0 {
                        try self.raw(", ")
                    }
                    try self.emitType(p)
                }
                try self.raw(")")
                // `callconv(…)` between the parameter list and the return,
                // Zig's slot order — one space each side, as on a signature.
                if ft.call_conv |cc| {
                    try self.raw(" callconv(")
                    try self.emitExpr(cc.*)
                    try self.raw(")")
                }
                if ft.ret |r| {
                    try self.raw(" ")
                    try self.emitType(r.*)
                }
            },
            .generic => |g| {
                try self.raw(g.name)
                try self.raw("(")
                for arg, idx in g.args {
                    if idx != 0 {
                        try self.raw(", ")
                    }
                    try self.emitExpr(arg)
                }
                try self.raw(")")
            },
            // `(A, B)` — canonical form: one space after each comma, no
            // trailing comma (a tolerated source trailing comma drops).
            .tuple => |elems| {
                try self.raw("(")
                for e, idx in elems {
                    if idx != 0 {
                        try self.raw(", ")
                    }
                    try self.emitType(e)
                }
                try self.raw(")")
            },
        }
    }

    // `asm [volatile] (…)` — inline assembly, the structure transposed from the
    // lowering (the template and constraint strings are a foreign sublanguage
    // that passes through unchanged; only the operand types and value expressions
    // take Flash spelling, via emitType / emitExpr). An asm output operand keeps
    // its `-> T` arrow, which the surface retains for this position.
    fn emitAsm(self *mut Printer, a ast.AsmExpr, depth usize) Error!void {
        try self.raw("asm ")
        if a.is_volatile {
            try self.raw("volatile ")
        }
        try self.raw("(")

        ml_template := a.template.* == .multiline_str
        multiline := ml_template || a.outputs.len > 0 || 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
        }

        if ml_template {
            try self.raw("\n")
            for ln in a.template.*.multiline_str {
                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.
        var n_sections usize = 0
        if a.clobbers != null {
            n_sections = 3
        } else if a.inputs.len > 0 {
            n_sections = 2
        } else if a.outputs.len > 0 {
            n_sections = 1
        }

        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
        }
        try self.indent(depth)
        try self.raw(")")
    }

    fn emitAsmOperandList(self *mut Printer, ops []mut ast.AsmOperand, depth usize) Error!void {
        if ops.len == 0 {
            try self.raw("\n")
            return
        }
        for op, idx in ops {
            if idx == 0 {
                try self.raw(" ")
            } else {
                try self.indent(depth + 1)
                try self.raw("  ")
            }
            try self.emitAsmOperand(op, depth)
            try self.raw(",\n")
        }
    }

    fn emitAsmOperand(self *mut Printer, 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 a space around the `..`, mirroring the lowering
// (a binary operation or a `catch` spaces it; every other form stays tight).
fn sliceBoundSpaces(x ast.Expr) bool {
    return switch x {
        .binary, .catch_expr => true,
        else => false,
    }
}

// Whether the source line immediately before byte `offset`'s line is blank
// (whitespace only) — the signal that the author left a paragraph break before
// the statement or comment at `offset`.
fn blankBeforeOffset(src []u8, offset usize) bool {
    var i = offset
    while i > 0 && src[i - 1] != '\n' {
        i -= 1 // back to the start of offset's line
    }
    if i == 0 {
        return false // first line of the file
    }
    nl := i - 1 // the '\n' ending the previous line
    var j = nl
    while j > 0 && src[j - 1] != '\n' {
        j -= 1 // back to the start of the previous line
    }
    var t = j
    while t < nl {
        c := src[t]
        if c != ' ' && c != '\t' && c != '\r' {
            return false
        }
        t += 1
    }
    return true
}

// The offset of the newline ending the line *before* `offset`'s line, or null
// when `offset` is already on the first line. Used to step a blank-line check up
// one line, for a construct whose first stored slice is one line below its head
// (a `comptime { … }` block, anchored at its first statement).
fn prevLineBreakOffset(src []u8, offset usize) ?usize {
    var i = offset
    while i > 0 && src[i - 1] != '\n' {
        i -= 1
    }
    if i == 0 {
        return null
    }
    return i - 1 // the '\n' that ends the previous line
}

// The source column of byte `offset` — the count of characters from the start of
// its line. A comment's column is how deeply it is indented.
fn commentColumn(src []u8, offset usize) usize {
    var i = offset
    while i > 0 && src[i - 1] != '\n' {
        i -= 1
    }
    return offset - i
}

// Whether the comment starting at `start` is a trailing comment — some
// non-whitespace byte precedes it on its own source line. Otherwise it is a
// standalone comment that occupies its line alone.
fn commentIsTrailing(src []u8, start usize) bool {
    var k = start
    while k > 0 && src[k - 1] != '\n' {
        c := src[k - 1]
        if c != ' ' && c != '\t' && c != '\r' {
            return true
        }
        k -= 1
    }
    return false
}

// Whether the source bytes in [from, to) contain no newline (the two offsets sit
// on the same physical line).
fn noNewlineBetween(src []u8, from usize, to usize) bool {
    if from > to || to > src.len {
        return false
    }
    var i = from
    while i < to {
        if src[i] == '\n' {
            return false
        }
        i += 1
    }
    return true
}

// The lead anchor of a top-level item — a source slice on the first line of its
// rendered form, including any leading doc comment (used to flush the comments
// that come before it).
fn itemLeadAnchor(it ast.Item) ?[]u8 {
    switch it {
        .use_decl => |u| return u.module,
        .link_decl => |l| return l.module,
        .const_decl => |c| return if (c.doc.len > 0) c.doc[0] else c.name,
        .fn_decl => |f| return if (f.doc.len > 0) f.doc[0] else f.name,
        .comptime_block => |stmts| return if (stmts.len > 0) stmtAnchor(stmts[0]) else null,
        // The quoted name lexeme is a source slice on the head line.
        .test_decl => |t| return t.name,
    }
}

// The tail anchor of a top-level item — a slice on the declaration's own first
// line (past any doc comment), used to attach a same-line trailing comment.
fn itemTailAnchor(it ast.Item) ?[]u8 {
    switch it {
        .use_decl => |u| return u.module,
        .link_decl => |l| return l.module,
        .const_decl => |c| return c.name,
        .fn_decl => |f| return f.name,
        .comptime_block => return null,
        .test_decl => |t| return t.name,
    }
}

// The lead / tail anchors of a container's associated declaration, as for items.
fn declLeadAnchor(d ast.ContainerDecl) ?[]u8 {
    switch d {
        .method => |m| return if (m.doc.len > 0) m.doc[0] else m.name,
        .constant => |c| return if (c.doc.len > 0) c.doc[0] else c.name,
        .use_import => |u| return u.module,
    }
}

fn declTailAnchor(d ast.ContainerDecl) ?[]u8 {
    switch d {
        .method => |m| return m.name,
        .constant => |c| return c.name,
        .use_import => |u| return u.module,
    }
}

// The boundary anchor of an `else` arm: its first statement's anchor. Null for
// an empty arm (`else {}`), which then simply keeps the enclosing boundary —
// also shielding the first-statement index from the empty slice.
fn elseAnchor(eb []ast.Stmt) ?[]u8 {
    return if (eb.len > 0) stmtAnchor(eb[0]) else null
}

// The boundary anchor of a switch prong: its first pattern. The `else` prong
// has no patterns; its body stands in — for a block body, the first statement
// (mirroring elseAnchor). Null falls back to the whole switch's boundary.
fn prongAnchor(p ast.SwitchProng) ?[]u8 {
    if p.patterns.len > 0 {
        return exprAnchor(p.patterns[0].lo)
    }
    switch p.body {
        .block_expr => |blk| return elseAnchor(blk.body),
        else => return exprAnchor(p.body),
    }
}

// A representative source slice on a statement's first physical line, used to
// recover its position for blank-line preservation. Null for the keyword-only
// forms (a bare `break` / `continue`) that store no anchor — they simply take no
// preserved blank.
fn stmtAnchor(s ast.Stmt) ?[]u8 {
    switch s {
        .discard => |x| return exprAnchor(x),
        .bind => |b| return b.name,
        // A destructure anchors on its first real name — a `_` skip stores no
        // source slice, and the same-line comma rule keeps every name on the
        // statement's first line anyway.
        .destructure => |d| {
            for maybe in d.names {
                if maybe |name| {
                    return name
                }
            }
            return null
        },
        .assign => |a| return exprAnchor(a.target),
        .destructure_assign => |da| return exprAnchor(da.targets[0]),
        .if_stmt => |iff| return exprAnchor(iff.cond),
        // A labeled loop's first lexeme is the label itself.
        .while_stmt => |w| return w.label orelse exprAnchor(w.cond),
        .for_stmt => |fr| return fr.label orelse if (fr.captures.len > 0) fr.captures[0] else exprAnchor(fr.iter),
        .defer_stmt => |inner| return stmtAnchor(inner.*),
        // An errdefer with a capture anchors on the capture name — its first
        // source slice after the keyword.
        .errdefer_stmt => |ed| return ed.capture orelse stmtAnchor(ed.body.*),
        // The block forms anchor on their first statement (like a top-level
        // comptime block); an empty block has no anchor.
        .defer_block => |stmts| return if (stmts.len > 0) stmtAnchor(stmts[0]) else null,
        .errdefer_block => |ed| return ed.capture orelse if (ed.body.len > 0) stmtAnchor(ed.body[0]) else null,
        .expr => |x| return exprAnchor(x),
    }
}

// The leftmost source slice of an expression (recursing into the head of a
// postfix / binary chain), or null for the forms whose head is a keyword or a
// synthesized node. Used only to locate a statement's first line.
fn exprAnchor(e ast.Expr) ?[]u8 {
    switch e {
        .int, .float, .string, .char, .ident, .value_word, .enum_lit, .error_lit => |s| return s,
        .multiline_str => |lines| return if (lines.len > 0) lines[0] else null,
        .member => |m| return exprAnchor(m.base.*),
        .deref => |d| return exprAnchor(d.*),
        .optional_unwrap => |u| return exprAnchor(u.*),
        .call => |c| return exprAnchor(c.callee.*),
        .index => |ix| return exprAnchor(ix.base.*),
        .slice => |sl| return exprAnchor(sl.base.*),
        .builtin_call => |b| return b.name,
        .unary => |u| return u.op,
        .binary => |b| return exprAnchor(b.lhs.*),
        .group => |g| return exprAnchor(g.*),
        .if_expr => |iff| return exprAnchor(iff.cond.*),
        .switch_expr => |sw| return exprAnchor(sw.subject.*),
        .try_expr => |t| return exprAnchor(t.*),
        .catch_expr => |c| return exprAnchor(c.lhs.*),
        .typed_lit => |tl| return exprAnchor(tl.type.*),
        // A `.{ … }` literal leads with `.{`, which is not a stored slice; use
        // its first field's name or value so a `return .{ … }` / `_ = .{ … }`
        // statement still has an anchor (without one, a leading comment would be
        // pushed past the statement instead of in front of it).
        .struct_lit => |fields| {
            if fields.len == 0 {
                return null
            }
            if fields[0].name |n| {
                return n
            }
            return exprAnchor(fields[0].value)
        },
        .error_set => |names| return if (names.len > 0) names[0] else null,
        .ret => |m| {
            if m |vals| {
                return exprAnchor(vals[0])
            }
            return null
        },
        .brk => |b| {
            if b.value |v| {
                return exprAnchor(v.*)
            }
            return null
        },
        else => return null,
    }
}

// --- tests ---------------------------------------------------------------
// Each test drives the whole pipeline — parse, render, and for the stability
// gate re-parse and re-lower — so the ported renderer is licensed by the same
// three guarantees the handwritten one carried: lowering invariance,
// idempotence, and comment-multiset preservation.

fn parseProg(arena sup.Allocator, src []u8) parser.Error!ast.Program {
    var p = parser.Parser.init(arena, src)
    return p.parseProgram()
}

fn lessStr(ctx i32, a []u8, b []u8) bool {
    _ = ctx
    return sup.lessThan(u8, a, b)
}

// The line-comment lexemes of `src`, sorted, for a multiset comparison.
fn sortedComments(arena sup.Allocator, src []u8) ![][]u8 {
    var list sup.List([]u8) = .empty
    var lx = Lexer.init(src)
    while true {
        t := lx.next()
        if t.kind == .eof {
            break
        }
        if t.kind == .line_comment {
            try list.append(arena, t.lexeme(src))
        }
    }
    slice := try list.toOwnedSlice(arena)
    const ctx i32 = 0
    sup.sort([]u8, slice, ctx, lessStr)
    return slice
}

// The three gates, run on any source (with or without comments): formatting
// never changes the emitted Zig (lower(parse(src)) == lower(parse(fmt(src)))),
// the formatter is idempotent (fmt(fmt(src)) == fmt(src)), and every comment in
// the input appears exactly once in the output (multiset equality).
fn expectStable(src []u8) !void {
    var a = sup.ArenaAllocator.init(sup.testAlloc)
    defer a.deinit()
    arena := a.allocator()
    lowered_src := try lower.emit(arena, try parseProg(arena, src))
    formatted := try format(arena, src)
    lowered_fmt := try lower.emit(arena, try parseProg(arena, formatted))
    try sup.expectEqualStrings(lowered_src, lowered_fmt)
    formatted2 := try format(arena, formatted)
    try sup.expectEqualStrings(formatted, formatted2)
    // comment multiset in == out
    in_comments := try sortedComments(arena, src)
    out_comments := try sortedComments(arena, formatted)
    try sup.expectEqual(in_comments.len, out_comments.len)
    var i usize = 0
    while i < in_comments.len {
        try sup.expectEqualStrings(in_comments[i], out_comments[i])
        i += 1
    }
}

fn expectFormat(src []u8, want []u8) !void {
    var a = sup.ArenaAllocator.init(sup.testAlloc)
    defer a.deinit()
    try sup.expectEqualStrings(want, try format(a.allocator(), src))
}
test "hello: imports, links, an exported entry, binds and calls" {
    try expectFormat("use flibc\n\nlink \"flibc_start\"\nlink \"flibc_mem\"\n\nexport fn main(_ usize, _ argv) noreturn {\n    const msg = \"hello from flash\\n\"\n    _ = flibc.sys.write_fd(1, msg.ptr, msg.len)\n    flibc.exit()\n}", "use flibc\n\nlink \"flibc_start\"\nlink \"flibc_mem\"\n\nexport fn main(_ usize, _ argv) noreturn {\n    msg := \"hello from flash\\n\"\n    _ = flibc.sys.write_fd(1, msg.ptr, msg.len)\n    flibc.exit()\n}\n")
}

test "types: pointer, slice, sentinel, optional, error-union, fn-type spellings round-trip" {
    try expectStable("fn pass(p *u32, q *mut u32, m []u8, w []mut u8, s [*:0]u8) *u32 {\n    return q\n}\n\nconst VTable = struct {\n    alloc *fn(*mut anyopaque, usize) ?[*]mut u8,\n    free *fn(*mut anyopaque, []mut u8) void,\n}\n\nfn dup(path cstr) AllocError!i32 {\n    return error.OutOfMemory\n}")
}

test "control flow: if/else-if, while-capture, range-for, switch, defer round-trip" {
    try expectStable("fn run(n usize) void {\n    for i in 0..n {\n        if i == 0 {\n            continue\n        } else if i == 1 {\n            defer cleanup()\n        } else {\n            work(i)\n        }\n    }\n    while it.next() |x| {\n        _ = x\n    }\n    switch tag {\n        0 => low(),\n        1, 2 => mid(),\n        else => high(),\n    }\n}")
}

test "pointer captures round-trip in every position" {
    try expectStable("fn f(arr *mut [4]u8, opt ?u8) void {\n    for *p in arr {\n        p.* = 0\n    }\n    for *p, i in arr {\n        p.* = #intCast(i)\n    }\n    if opt |*x| {\n        x.* += 1\n    }\n    while it.next() |*v| {\n        v.* = 0\n    }\n    switch u {\n        .a => |*pay| {\n            pay.* = 1\n        },\n        else => {},\n    }\n}")
}

test "a spaced pointer capture canonicalises to the tight `|*x|` / `for *p`" {
    try expectFormat("fn f(arr *mut [4]u8, opt ?u8) void {\n    for * p in arr {\n        p.* = 0\n    }\n    if opt | * x | {\n        x.* += 1\n    }\n}", "fn f(arr *mut [4]u8, opt ?u8) void {\n    for *p in arr {\n        p.* = 0\n    }\n    if opt |*x| {\n        x.* += 1\n    }\n}\n")
}

test "containers: struct with fields and a method, enum, tagged union round-trip" {
    try expectStable("const Point = struct {\n    x i32,\n    y i32 = 0,\n\n    fn sum(self Point) i32 {\n        return self.x + self.y\n    }\n}\n\nconst Color = enum(u8) {\n    red,\n    green = 5,\n    blue,\n}\n\nconst Tok = union(enum) {\n    eof,\n    int usize,\n}")
}

test "enum and union bodies with methods, constants, and imports round-trip" {
    try expectStable("const Color = enum(u8) {\n    red,\n    green = 5,\n\n    use \"names\" as names\n\n    const COUNT usize = 2\n\n    /// the canonical default\n    pub fn default() Color {\n        return .red\n    }\n}\n\nconst Tok = union(enum) {\n    eof,\n    int usize,\n\n    fn isEof(self Tok) bool {\n        return self == .eof\n    }\n}")
}

test "expressions: builtins, logical operators, casts, struct literals round-trip" {
    try expectStable("fn f(a bool, b bool) usize {\n    if a && b || c {\n        return #intCast(x)\n    }\n    p := P{ .x = 1, .y = 2 }\n    q := .{ 1, 2, 3 }\n    return value orelse 0\n}")
}

test "wrapping compound assignments round-trip verbatim" {
    try expectStable("fn f(s *mut S) {\n    var i u32 = 0\n    i +%= 1\n    i -%= 2\n    i *%= 3\n    s.head +%= i -% 1\n}")
}

test "array repetition `**` round-trips verbatim" {
    try expectStable("pub const SIZE u64 = 4096\n\npub const KlogRing = struct {\n    buf [SIZE]u8 = [_]u8{0} ** SIZE,\n    head u64 = 0,\n}\n\nvar pool [1024 * 1024]u8 = [_]u8{0} ** (1024 * 1024)")
}

test "export var and extern var round-trip verbatim" {
    try expectStable("export var nr_tasks i32 = 0\n\npub export var next_pid i32 = 1\n\nextern var _kernel_pa_end u8\n\npub extern var __initramfs_start u8")
}

test "linksection round-trips verbatim on bindings and fn signatures" {
    try expectStable("var scratch [64]u8 linksection(\".sdscratch\") = undefined\n\nconst PAD [4096]u8 linksection(\".rodata\") = .{0xAB} ** 4096\n\nconst named linksection(SECTION) = 1\n\nfn fast() linksection(\".fast\") {}\n\nfn shim() linksection(\".vec\") callconv(.c) u32 {\n    return 0\n}")
}

test "packed and extern struct layouts round-trip verbatim" {
    try expectStable("pub const DirEntry = packed struct {\n    attr u8,\n    file_size u32,\n}\n\npub const Dirent = extern struct {\n    name [32]u8 = .{0} ** 32,\n    d_type u8 = 0,\n}\n\nconst Ops = extern struct {\n    count u32,\n\n    pub fn empty() Ops {\n        return Ops{ .count = 0 }\n    }\n}")
}

test "doc comments are preserved on the declaration they lead" {
    try expectStable("/// the maximum\n/// width\npub const MAX = 80\n\n/// add two numbers\nfn add(a i32, b i32) i32 {\n    return a + b\n}")
}

test "blank lines between statements are preserved, collapsed to one" {
    try expectFormat("fn f() void {\n    a()\n\n\n    b()\n    c()\n}", "fn f() void {\n    a()\n\n    b()\n    c()\n}\n")
}

test "value if-expression keeps its parentheses" {
    try expectStable("fn pick(c bool) usize {\n    return if (c) 1 else 2\n}")
}

test "the := canon: a plain bind rewrites; typed, var, comptime keep their keyword" {
    try expectFormat("fn f() void {\n    const b = 2\n    const c i32 = 3\n    var d = 4\n    comptime const e = 5\n}", "fn f() void {\n    b := 2\n    const c i32 = 3\n    var d = 4\n    comptime const e = 5\n}\n")
}

test "the := canon round-trips and stays stable across binding kinds" {
    try expectStable("fn f() void {\n    a := compute()\n    const b = other()\n    const c usize = 3\n    var d = 4\n    const g usize align(16) = 6\n}")
}

test "a standalone comment leads a statement; a trailing one rides its line" {
    try expectFormat("fn f() void {\n    // compute the sum\n    s := a + b // the running total\n    return s\n}", "fn f() void {\n    // compute the sum\n    s := a + b // the running total\n    return s\n}\n")
}

test "trailing comments ride enum variants" {
    try expectFormat("const Kind = enum {\n    command, // the first token\n    path, // a later token\n}", "const Kind = enum {\n    command, // the first token\n    path, // a later token\n}\n")
}

test "a file-header block and a doc comment are both preserved" {
    try expectFormat("// header line one\n// header line two\n\n/// a doc\npub const MAX = 80", "// header line one\n// header line two\n\n/// a doc\npub const MAX = 80\n")
}

test "a blank line before a top-level comptime block is preserved" {
    try expectFormat("fn shim() void {\n    work()\n}\n\ncomptime {\n    #export(&shim, .{ .name = \"_start\" })\n}", "fn shim() void {\n    work()\n}\n\ncomptime {\n    #export(&shim, .{ .name = \"_start\" })\n}\n")
}

test "a module-head //! comment leads the file" {
    try expectFormat("//! module documentation\n\nuse flibc", "//! module documentation\n\nuse flibc\n")
}

test "a comment-only file emits its comments" {
    try expectFormat("// just a comment\n// and another", "// just a comment\n// and another\n")
}

test "a block-final comment stays inside the block" {
    try expectFormat("fn f() void {\n    work()\n    // trailing note inside the block\n}", "fn f() void {\n    work()\n    // trailing note inside the block\n}\n")
}

test "consecutive top-level declarations keep the author's blank-line grouping" {
    try expectFormat("pub const A = x.A\npub const B = x.B\n\npub const C = x.C", "pub const A = x.A\npub const B = x.B\n\npub const C = x.C\n")
}

test "a comment leads a return-struct-literal statement, not pushed past it" {
    try expectFormat("fn f() T {\n    // build the result\n    return .{ .key = .none }\n}", "fn f() T {\n    // build the result\n    return .{ .key = .none }\n}\n")
}

test "a trailing comment on a method's statement stays in that method" {
    try expectFormat("const S = struct {\n    fn a(self S) void {\n        return\n    }\n\n    fn b(self S) void {\n        v := f() // a trailing note\n    }\n}", "const S = struct {\n    fn a(self S) void {\n        return\n    }\n\n    fn b(self S) void {\n        v := f() // a trailing note\n    }\n}\n")
}

test "comment-rich source: every comment survives, output is stable" {
    try expectStable("// a leading file comment\n\nuse flibc // the C runtime\n\n/// the entry\nexport fn main(_ usize, _ argv) noreturn {\n    // set up\n    n := count() // how many\n    for i in 0..n {\n        // each iteration\n        step(i)\n    }\n    // tear down\n    flibc.exit()\n}")
}

test "composite-type alias declarations round-trip" {
    try expectStable("const F = *fn(u8) u8\nconst O = ?u8\nconst S = []u8\nconst M = *mut fn() void\n\nfn take(g Get([]u8)) void {\n    _ = g\n}")
}

test "a function-type callconv round-trips in every position" {
    try expectStable("pub const VfsOps = extern struct {\n    open *fn(*SuperBlock, [*]u8, usize) callconv(.c) i32,\n    close *fn(*SuperBlock) callconv(.c),\n    readdir ?*fn(*SuperBlock, u64) callconv(.c) i32 = null,\n}\n\nconst F = *fn(u8) callconv(.naked) noreturn")
}

test "defer/errdefer block form round-trips, comments riding inside" {
    try expectStable("fn run(fd i32) !void {\n    defer {\n        // release in reverse order\n        close(fd)\n        close(fd + 1)\n    }\n    errdefer {\n        close(0)\n    }\n    defer close(fd)\n    return\n}")
}

test "errdefer capture round-trips, a spaced capture canonicalises" {
    try expectStable("fn run(fd i32) !void {\n    errdefer |err| log(err)\n    errdefer |err| {\n        // the unwinding error\n        log(err)\n    }\n    errdefer close(fd)\n    return\n}")
    try expectFormat("fn run() !void {\n    errdefer | err | log(err)\n}", "fn run() !void {\n    errdefer |err| log(err)\n}\n")
}

test "test blocks round-trip, comments riding inside" {
    try expectStable("// suite header\nuse std\n\ntest \"first\" {\n    // inside the body\n    n := 1\n    _ = n\n}\n\ntest \"empty\" {}")
}

test "loop else arms and the if else-capture round-trip, comments riding inside" {
    try expectStable("fn f(xs []u8, c bool) void {\n    if next() |v| {\n        consume(v)\n    } else |err| {\n        // the failure arm\n        log(err)\n    }\n    while next() |v| {\n        consume(v)\n    } else |err| {\n        log(err)\n    }\n    while c {\n        // body comment stays in the body\n        step()\n    } else {\n        done()\n    }\n    for x in xs {\n        consume(x)\n    } else {\n        done()\n    }\n}")
}

test "inline loops round-trip: inline for across its shapes, inline while unchanged" {
    try expectStable("fn f(xs []u8, n usize) void {\n    inline for x in xs {\n        consume(x)\n    }\n    inline for i in 0..n {\n        consume(i)\n    } else {\n        done()\n    }\n    inline for x, i in xs {\n        // comment rides the unrolled body\n        consume(i)\n    }\n    inline while n > 0 {\n        step()\n    }\n}")
}

test "an empty else arm round-trips (the elseAnchor guard)" {
    try expectStable("fn f(xs []u8, c bool) void {\n    if c {} else {}\n    while c {} else {}\n    for x in xs {} else {}\n}")
}

test "tuple types and multi-return round-trip, comments riding inside" {
    try expectStable("const Pair = (u8, bool)\n\nfn pair() (u8, bool) {\n    // both spellings hold\n    return 42, true\n}\n\nfn lit() Pair {\n    return .{ 7, false }\n}\n\nfn first(t (u8, (u8, bool))) u8 {\n    return (t[0] + t[1][0]) * 1\n}")
}

test "a tuple type's trailing comma drops to the canonical spelling" {
    try expectFormat("fn pair() (u8, bool,) {\n    return 42, true\n}", "fn pair() (u8, bool) {\n    return 42, true\n}\n")
}

test "destructures round-trip, comments riding inside" {
    try expectStable("fn pair() (u8, bool) {\n    return 42, true\n}\n\nfn demo() void {\n    // both skips hold\n    tok, _ := pair()\n    _, ok := pair()\n    var x, y = pair()\n    x, y = pair() // the assignment list is verbatim\n    arr[0], y = pair()\n    _ = tok\n    _ = ok\n    _ = x\n}")
}

test "the ':=' canon extends to destructures: 'const' rewrites, 'var' keeps its keyword" {
    try expectFormat("fn demo() void {\n    const a, b = pair()\n    var x, y = pair()\n    _ = .{ a, b, x, y }\n}", "fn demo() void {\n    a, b := pair()\n    var x, y = pair()\n    _ = .{ a, b, x, y }\n}\n")
}

test "labeled loops round-trip: while, for, inline order, break and continue targets" {
    try expectStable("fn f(xs []u8) usize {\n    outer: while true {\n        for x, i in xs {\n            if x == 0 {\n                break :outer\n            }\n            _ = i\n        }\n    }\n    return 0\n}\n\nfn g(xs []u8) void {\n    scan: for x in xs {\n        if x == 0 {\n            continue :scan\n        }\n        _ = x\n    }\n}\n\nfn h() void {\n    un: inline while true {\n        break :un\n    }\n}")
}

test "a spaced loop label canonicalises to 'label: loop'" {
    // `outer : while` — token-stream parsing makes the spacing free; the canon
    // is `outer: while`, matching the labeled-block print.
    try expectFormat("fn f() void {\n    outer : while true {\n        break :outer\n    }\n}", "fn f() void {\n    outer: while true {\n        break :outer\n    }\n}\n")
}

test "a comment above a labeled loop anchors on the label line" {
    try expectStable("fn f() void {\n    // the pick loop\n    outer: while true {\n        break :outer\n    }\n}")
}

test "type-position and file-scope align round-trip" {
    try expectStable("extern fn a0(x []align(16) u8, y []align(16) mut u8, z [:0]align(2) u8)\n\nextern fn a1(x [*]align(8) mut volatile u8, y [*:0]align(2) mut u8, z *align(4) mut volatile u32)\n\nvar pool [4096]u8 align(4096) = undefined\n\nvar head align(64) = compute()\n\nextern var _vec u8 align(2048)\n\nvar pad [16]u8 align(4) linksection(\".rodata\") = undefined")
}

test "a spaced type-position align canonicalises tight" {
    // `align ( 16 )` re-emits as `align(16) ` — tight parens, one trailing
    // space before the qualifiers/element, on both the type-position and
    // the file-scope bind spelling.
    try expectFormat("extern fn f(x []align ( 16 ) mut u8)\n\nvar pool [64]u8 align ( 64 ) = undefined", "extern fn f(x []align(16) mut u8)\n\nvar pool [64]u8 align(64) = undefined\n")
}