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