// 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 ` ` 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") }