Flash 736 lines
// Flash AST — node definitions for the Flash grammar.
//
// The shapes below mirror the grammar: a Flash program is a flat list of
// top-level items (imports, link directives, function definitions), and a
// function body is a flat list of statements over a small expression grammar.
// The parser populates these nodes and the lowering stage walks them to Zig
// text. Every string field is a byte-slice into the original source, so the
// AST copies no text. This invariant is load-bearing beyond zero-copy: the
// semantic checker recovers a diagnostic's line and column from a slice's
// address — its offset into the source buffer (see sema.zig `locate`) — so a
// string field MUST stay a real slice into the original source, never a
// synthesized or reallocated string. Everything here grows as the language
// gains syntax.
use "support" as sup
pub const Program = struct {
items []mut Item,
}
pub const Item = union(enum) {
use_decl UseDecl,
link_decl LinkDecl,
fn_decl FnDecl,
const_decl ConstDecl,
// `comptime { … }` — a top-level comptime block. Its body is an ordinary
// statement list (today a lone `@export(…)` that forces a symbol's
// emission, e.g. the `_start` shim). Lowers to `comptime { … }` verbatim.
comptime_block []mut Stmt,
test_decl TestDecl,
}
// `test "name" { … }` — a test block: a string-named statement body that runs
// under the test harness (`zig build test-flash`), lowering one-to-one to a
// Zig `test` block. The name is the verbatim string-literal lexeme, quotes
// included, so it re-emits byte-for-byte (and stays a real source slice for
// diagnostics and the formatter's anchors). A test declares no binding — the
// name is not an identifier and cannot be referenced.
pub const TestDecl = struct {
name []u8, // the "…" lexeme, quotes included
body []mut Stmt,
}
// A top-level named constant: `const NAME = expr` or `const NAME T = expr`.
// The block-level `const`/`var` binding is a Stmt (see Bind); this is the
// item-level form that lowers to a file-scope `const NAME: T = expr;` (or
// `var NAME: T = expr;` when `is_mut`, a mutable global). A file-scope `var`
// also takes the fn-modifier matrix: `export var` defines a cross-object
// symbol, `extern var` consumes one (typed, with no initializer).
pub const ConstDecl = struct {
// Leading `///` doc-comment lines, each the verbatim bytes after the three
// slashes; an empty slice means no doc comment. Re-emitted before the
// declaration (see lower.zig), byte-for-byte as zig fmt keeps them.
doc [][]u8,
is_pub bool, // `pub const` / `pub var` — public visibility for an importing module
is_export bool, // `export var` — the binding defines a cross-object symbol
is_extern bool, // `extern var` — storage defined in another object (value == null)
is_mut bool, // `var` — a mutable file-scope binding (`const` is the immutable default)
name []u8,
type ?TypeRef,
// An explicit `align(expr)` qualifier between the type and any
// `linksection` — the symbol's storage alignment (`var pool [N]u8
// align(4096) = …`), Zig's slot order. The inner expression is lowered
// verbatim. Null when absent. Valid on an `extern var` too (the
// alignment is a promise about the externally-defined symbol, as in Zig).
align_expr ?Expr = null,
// An explicit `linksection("…")` attribute between any `align` and `=` —
// the section the symbol is placed in (a NOLOAD scratch region, a
// .rodata pad). The inner expression is lowered verbatim, in Zig's slot
// order (after `align`). Null when absent; never set on an
// `extern var` (the defining object owns the section).
link_section ?Expr,
value ?Expr, // null only for `extern var` (bodyless/valueless, like `extern fn`)
}
pub const UseDecl = struct {
is_pub bool, // `pub use` — re-export the import to importing modules (`pub const … = @import(…)`)
// The @import stem. A bare module name (`use flibc` → "flibc") passes through
// verbatim; a quoted file import (`use "syscalls" as sys`) stores the
// extensionless stem ("syscalls") and lowering appends the backend artifact
// suffix (`.zig`). Frozen Flash source therefore never names a file
// extension — `is_file` records which spelling produced this decl.
module []u8,
alias ?[]u8, // `use X as Y` / `use "X" as Y`
is_file bool, // a quoted file import (`use "X" as Y`, a sibling file) vs a bare module name
}
pub const LinkDecl = struct {
module []u8, // the string-literal payload
}
pub const FnDecl = struct {
doc [][]u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
is_pub bool, // `pub fn` — public visibility (precedes `export`/`extern`/`inline`)
is_export bool, // `export fn` — a C-ABI boundary, emitted with callconv(.c)
is_extern bool, // `extern fn` — a bodyless C-ABI prototype (body == null); shares the
// `export`/`inline` prototype slot, so none of the three co-occur
is_inline bool, // `inline fn` — forced inlining; shares the prototype slot
// with `export`, so the two never co-occur (Zig rejects `export inline fn`)
name []u8,
params []mut Param,
ret ?TypeRef,
// An explicit `linksection("…")` attribute between the parameter list and
// any `callconv(…)` (Zig's slot order) — the section the function is
// placed in. The inner expression is lowered verbatim; null when absent.
link_section ?Expr,
// An explicit calling convention `callconv(.c)` written in the signature —
// the inner expression (an inferred enum literal). Null when absent; an
// `export fn` emits callconv(.c) without one. Lowered as ` callconv(<conv>)`.
call_conv ?Expr,
body ?[]mut Stmt, // null == a bodyless prototype (`extern fn …;`)
}
pub const Param = struct {
is_comptime bool, // `comptime x: T` — a compile-time parameter (lowered with a `comptime ` prefix)
name ?[]u8, // null == the `_` placeholder
type TypeRef,
}
pub const TypeRef = union(enum) {
name []u8, // usize, u8, void, noreturn, argv, cstr, or an imported A.B
slice PtrElem, // []T / []align(A) T (const by default; see lower.zig)
slice_mut PtrElem, // []mut T
slice_sentinel SentinelPtr, // [:s]T — a sentinel-terminated slice (const by default)
slice_sentinel_mut SentinelPtr, // [:s]mut T — its mutable form
many_ptr PtrElem, // [*]T — a many-item pointer, const pointee by default (see lower.zig)
many_ptr_mut PtrElem, // [*]mut T — a many-item pointer to a mutable pointee
many_ptr_volatile PtrElem, // [*]volatile T — const+volatile pointee (see lower.zig)
many_ptr_mut_volatile PtrElem, // [*]mut volatile T — mutable+volatile pointee
many_ptr_sentinel SentinelPtr, // [*:s]T — a sentinel-terminated many-item pointer (const by default)
many_ptr_sentinel_mut SentinelPtr, // [*:s]mut T — its mutable form
ptr PtrElem, // *T — a single-item pointer, const pointee by default (see lower.zig)
ptr_mut PtrElem, // *mut T — a single-item pointer to a mutable pointee
ptr_volatile PtrElem, // *volatile T — const+volatile pointee (see lower.zig)
ptr_mut_volatile PtrElem, // *mut volatile T — mutable+volatile pointee
array Array, // [N]T (N is a length expression)
array_sentinel ArraySentinel, // [N:s]T — a fixed-length sentinel-terminated array
array_inferred *mut TypeRef, // [_]T — an array whose length is inferred from its initializer
array_inferred_sentinel SentinelPtr, // [_:s]T — its sentinel-terminated form ([_:s]T{ … })
optional *mut TypeRef, // ?T — an optional, unwrapped with `orelse` / capture
errunion ErrUnion, // E!T / !T — an error union, propagated with `try` / `catch`
fn_type FnType, // fn(P, …) R — a function type; wrap in `*`/`*mut` for a pointer
generic GenericType, // Name(args…) — a generic type applied in type position
// `(A, B)` — a tuple type: parenthesized, comma-separated element types,
// arity ≥ 2 (a one-element `(T)` is expression grouping, not a tuple).
// Lowers to Zig's inline positional struct `struct { A, B }`; the value
// spelling stays the anonymous literal `.{ … }` and element access the
// postfix index `t[0]`.
tuple []mut TypeRef,
}
// `E!T` / `!T` — an error union: a payload type `T` paired with an error set.
// `set` names the set explicitly (the infix `E!T`, where `E` is an error-set
// type — `error{…}` or a named alias). `set == null` is the prefix `!T`, whose
// set the compiler *infers*; Zig allows an inferred set only on a function
// *declaration's* return type, so the parser rejects it on a function *type*
// (`*fn(…) !T`). Both lower verbatim: `set!payload`, or `!payload` inferred.
pub const ErrUnion = struct {
set ?*mut TypeRef,
payload *mut TypeRef,
}
// The payload of the plain (non-sentinel) pointer/slice forms: the pointee
// type plus an optional `align(expr)` qualifier directly after the prefix —
// `[]align(16) u8`, `*align(4) mut T` — Zig's slot (before `const`/`volatile`
// there, before `mut`/`volatile` here). The expression is lowered verbatim.
pub const PtrElem = struct {
align_expr ?*mut Expr = null,
elem *mut TypeRef,
}
pub const Array = struct {
len *mut Expr, // the length expression, e.g. 512 or pkg.BUF_LEN
elem *mut TypeRef,
}
// `[N:s]T` — a fixed-length array terminated by a sentinel value: `len` elements
// of `elem` with a trailing `sentinel` (Zig guarantees `array[len] == sentinel`).
// The inferred-length form `[_:s]T` drops `len` and reuses SentinelPtr below.
pub const ArraySentinel = struct {
len *mut Expr,
sentinel *mut Expr,
elem *mut TypeRef,
}
// `[*:s]T` — a many-item pointer terminated by a sentinel value. `sentinel` is
// the terminator expression (commonly `0`), `elem` the pointee element type.
// Like `[*]T`, the element is not const by default; the prefix composes under
// `*` / `[N]` / `?` as any other element type does. The structurally-identical
// sentinel-terminated *slice* forms (`[:s]T` / `[:s]mut T`) and the
// inferred-length sentinel *array* (`[_:s]T`) reuse this payload.
// `align_expr` is the optional `align(expr)` qualifier after the closing `]`
// (`[*:0]align(16) u8` — Zig's slot, before `mut`); it is never set on the
// array form (an array type carries no `align` — align the binding instead).
pub const SentinelPtr = struct {
sentinel *mut Expr,
align_expr ?*mut Expr = null,
elem *mut TypeRef,
}
// `fn(P, …) R` — a function *type*: an ordered list of (unnamed) parameter
// types and an optional return type. It is not itself a storable value; a
// function *pointer* is this type behind a `*` (`*fn(…) R` lowers to
// `*const fn (…) R`, const-pointee by default like any `*T`; `*mut fn(…)` for a
// mutable one). The pointer-ness lives in the surrounding `ptr`/`ptr_mut`, never
// here. Parameters are bare types — a function type names no bindings — so the
// surface mirrors Go's `func(int, string) error`, not a named parameter list.
pub const FnType = struct {
params []mut TypeRef, // parameter types, in source order; empty == `fn () R`
// An explicit calling convention `callconv(.c)` between the parameter list
// and the return type — Zig's slot order, mirroring FnDecl.call_conv. Boxed:
// an Expr held by value here would close a by-value cycle through
// `Expr.asm_expr` (AsmExpr carries a TypeRef by value). Null when absent.
call_conv ?*mut Expr,
ret ?*mut TypeRef, // return type; null lowers to `void`
}
// `Name(args…)` — a generic type applied in type position (`List(u8)`,
// `Map(K, V)`, `pkg.Ring(64)`), so a generic instance can be named where a
// type is expected (parameter, field, return). The arguments are full
// expressions, exactly as a value-position call `List(u8)` parses them — a
// type-name argument is an identifier expression, a value argument (`Ring(64)`)
// is a literal, and a composite-type argument (`List([]u8)`, `Map(u8, *fn() u8)`)
// is a `type_lit` expression (a `[`/`?`/`*`/`fn`-led type in value position).
// `name` is the verbatim (possibly dotted) generic name; it lowers as
// `name(args…)`.
pub const GenericType = struct {
name []u8,
args []mut Expr,
}
pub const Stmt = union(enum) {
discard Expr, // `_ = expr`
bind Bind, // `:=` / `var` / `const`
destructure Destructure, // `a, b := e` / `var a, b = e` — a multi-name bind
assign Assign, // `target op= value` (op is "=", "+=", …)
destructure_assign DestructureAssign, // `a, b = e` — store to existing lvalues
if_stmt If, // `if cond { … } else { … }`
while_stmt While, // `while cond { … }` / `while cond |x| { … }`
for_stmt For, // `for item in iter { … }`
defer_stmt *mut Stmt, // `defer <stmt>` — runs the inner statement on scope exit
errdefer_stmt Errdefer, // `errdefer [|err|] <stmt>` — runs it only on an error exit
defer_block []mut Stmt, // `defer { … }` — runs the block on scope exit
errdefer_block ErrdeferBlock, // `errdefer [|err|] { … }` — runs it only on an error exit
expr Expr, // a bare call, break, continue, return, etc.
}
// `errdefer <stmt>` with an optional `|err|` capture (`errdefer |err|
// log(err)`) — binds the error that is unwinding, in scope for the deferred
// statement only. The error binds by value (`|*err|` is rejected, as in Zig);
// `capture` is null on the plain form. Plain `defer` takes no capture — there
// is no error on a normal exit.
pub const Errdefer = struct {
capture ?[]u8,
body *mut Stmt,
}
// The block form, `errdefer [|err|] { … }` — the same optional capture, in
// scope for the whole deferred block.
pub const ErrdeferBlock = struct {
capture ?[]u8,
body []mut Stmt,
}
// `a, b := expr` (immutable, the `:=` canon) or `var a, b = expr` /
// `const a, b = expr` (the keyword form) — a destructuring bind over a
// tuple-valued expression. One keyword rules all names: every name shares
// `is_mut`. A `_` entry (a null name) skips that position; at least one entry
// is a real name (an all-underscore destructure is rejected — that is
// `_ = expr`). No per-name type or `align` is spellable — annotate the
// producer instead. Lowers to Zig's native destructure, the binding keyword
// repeated per name: `const a, const b = expr;` / `var a, var b = expr;`,
// a skip staying `_`.
pub const Destructure = struct {
is_mut bool,
names []mut ?[]u8, // null == `_` (skip); len ≥ 2, at least one non-null
value Expr,
}
// `a, b = expr` — a destructuring assignment onto existing lvalues (member,
// index, and deref targets included). `=` is the only operator — a compound
// op cannot destructure — and `_` is not a target (skipping is the bind
// form's job). Lowers verbatim: `a, b = expr;`.
pub const DestructureAssign = struct {
targets []mut Expr, // len ≥ 2, each an lvalue expression
value Expr,
}
pub const Bind = struct {
is_mut bool,
is_comptime bool, // `comptime var` / `comptime const` — a compile-time local (lowered with a `comptime ` prefix)
name []u8,
type ?TypeRef,
// An `align(expr)` qualifier (`const x T align(16) = …`), between the type
// and `=`; null when absent. Lowered verbatim as ` align(<expr>)`.
align_expr ?Expr,
value Expr,
}
// An assignment statement. `op` is the verbatim operator lexeme — "=" for a
// plain store, or one of the compound forms "+=", "-=", "*=", "/=", "%=", "&=",
// "|=", "^=", "<<=", ">>="; all of these are spelled identically in the emitted
// Zig.
pub const Assign = struct {
target Expr,
op []u8,
value Expr,
}
pub const If = struct {
cond Expr,
// An optional-capture if: `if opt |x| { … }` binds `x` to the unwrapped
// payload for the body, lowering to Zig's `if (opt) |x| { … }`. Null for a
// plain boolean `if`.
capture ?[]u8,
// `if opt |*x| { … }` — a pointer capture: `x` binds a pointer to the
// payload so the body can write through it (`x.* = v`). The name stays the
// bare identifier (a diagnostic anchor must be a real source slice), the
// flag carries the `*`. The else arm's error capture binds by value only
// (matching Zig), so it takes no flag.
capture_is_ptr bool = false,
body []mut Stmt,
// The `else` arm, if present. An `else if` chain is encoded as a one-element
// body holding a nested `if_stmt`; lowering renders that as idiomatic Zig
// `else if`.
else_body ?[]mut Stmt,
// The error capture on the else arm: `if expr |x| { … } else |err| { … }`
// binds the failed error union's error for the else body, lowering to Zig's
// `else |err| { … }`. Only set together with `else_body`; a captured else
// arm is always a block, never an `else if` chain.
else_capture ?[]u8,
}
pub const While = struct {
is_inline bool, // `inline while` — a compile-time-unrolled loop (lowered with an `inline ` prefix)
// `outer: while cond { … }` — an optional loop label, the target of a
// `break :outer` / `continue :outer` inside the body. The bare lexeme (the
// declaring source slice — it doubles as the Diag anchor); lowering renders
// it as Zig's `outer: while (…)` prefix.
label ?[]u8 = null,
cond Expr,
// An optional payload capture `while expr |x| { … }`: binds the unwrapped
// optional / error-union payload for the body, lowering to Zig's iterator
// `while (expr) |x| { … }`. Null for a plain boolean `while`. Shaped exactly
// like the `if` capture — the same `| ident | {` lookahead stops the
// condition parse before it.
capture ?[]u8,
// `while expr |*x| { … }` — a pointer capture on the payload; same shape
// and rules as the `if` flag (the else arm's error capture takes none).
capture_is_ptr bool = false,
body []mut Stmt,
// The loop `else` arm: runs when the loop ends without `break` (condition
// false / payload exhausted), lowering to Zig's `while (…) … else { … }`.
else_body ?[]mut Stmt,
// The error capture on the else arm — `while next() |x| { … } else |err|
// { … }`, the error-union iterator's failure binding. Only set together
// with `else_body`.
else_capture ?[]u8,
}
// `for cap in iter { … }` — iterate `iter`, binding each element to `cap`. Two
// surface forms ride on the bare element loop:
// * a range iterator `for i in lo..hi` sets `range_hi`, lowering to Zig's
// `for (lo..hi) |i|` — the counted loop without the `while` ceremony;
// * a second capture `for x, i in xs` indexes the iteration, lowering to
// `for (xs, 0..) |x, i|` (the trailing `0..` index range is implicit,
// supplied at lowering).
// `captures` holds one name (the element) or two (element, then index). `iter`
// is the iterable, or the range's low bound when `range_hi` is set.
pub const For = struct {
is_inline bool, // `inline for` — a compile-time-unrolled loop (lowered with an `inline ` prefix)
// `outer: for … { … }` — an optional loop label, exactly as on `While`.
label ?[]u8 = null,
captures []mut []u8,
// `for *p in &arr { … }` — a pointer capture on the ELEMENT: `p` binds a
// pointer to each element so the body can write through it, lowering to
// Zig's `for (&arr) |*p|`. Only the element capture can be a pointer (the
// index is a value — matching Zig); `captures` keeps the bare names.
elem_is_ptr bool = false,
iter Expr,
range_hi ?Expr, // set → `iter` is a range's low bound and this its high bound
body []mut Stmt,
// The loop `else` arm: runs when the iteration completes without `break`,
// lowering to Zig's `for (…) |…| { … } else { … }`. A `for` else takes no
// capture (there is no error to bind — matching Zig).
else_body ?[]mut Stmt,
}
pub const Expr = union(enum) {
int []u8, // decimal, 0x hex, 0o octal, or 0b binary integer; verbatim
float []u8, // decimal float, e.g. 3.14 or 1.5e-3; verbatim
string []u8,
// A `\\…` raw multiline string: one entry per source line, each the bytes
// after that line's `\\` (no escape processing). Lowering re-emits them as a
// Zig multiline string, joined by newlines.
multiline_str []mut []u8,
char []u8, // 'c' (lexeme including quotes)
ident []u8,
// A reserved value keyword — `true`, `false`, `null`, `undefined`, or
// `unreachable`. Held apart from `ident` so these words are never bound as
// names (the lexer yields them as keywords, not identifiers) and so a later
// check can forbid `undefined` as a const value. Lowers verbatim.
value_word []u8,
member Member, // expr.field
deref *mut Expr, // expr.* — single-item pointer dereference (also a valid lvalue)
optional_unwrap *mut Expr, // expr.? — optional unwrap (assert non-null); same postfix slot as `.*`
call Call, // expr(args)
index Index, // expr[i]
slice Slice, // expr[lo..hi] / expr[lo..] / expr[lo..hi :s] (sentinel-terminated)
builtin_call BuiltinCall, // #name(args)
unary Unary, // prefix op: ! - &
binary Binary, // lhs op rhs
struct_lit []mut StructLitField, // .{ a, b } / .{ .x = 1 } — empty slice == .{}
typed_lit TypedLit, // Name{ .x = 1 } / Name{} — a type-prefixed initializer
type_lit *mut TypeRef, // a composite type in value position — a type-alias value
// (`const F = *fn(u8) u8`), a generic argument (`List([]u8)`), or the head of
// an array-typed literal (`[_]u8{ … }`); a `[`/`?`/`*`/`fn`-led type, distinct
// from a named type that parses as `ident`
enum_lit []u8, // .red — an inferred enum literal (the bare variant name)
error_lit []u8, // error.Name — an error-value origination (the bare error name)
error_set []mut []u8, // error{ A, B } — a named error-set definition (member names)
block_expr BlockExpr, // label: { … } — a labeled block whose value is a `break :label v`
struct_def StructDef, // struct { field T, … } — a struct type definition
enum_def EnumDef, // enum { a, b } / enum(u8) { … } — an enum type definition
union_def UnionDef, // union(enum) { a, b T, … } — a tagged-union type definition
group *mut Expr, // ( expr ) — explicit parentheses, preserved so the
// emitted Zig keeps the programmer's evaluation order verbatim
if_expr IfExpr, // `if cond a else b` — an if used for its value (both arms required)
switch_expr SwitchExpr, // `switch subj { pat => body, … }` — a switch over the subject
try_expr *mut Expr, // `try expr` — unwrap an error union, propagate on error
catch_expr Catch, // `expr catch [|e|] handler` — recover from an error
asm_expr AsmExpr, // `asm [volatile] (template [: out : in : clobbers])` — inline assembly
// Control-transfer forms are expressions (as in Zig) so they compose on the
// right of `orelse` — e.g. `argv[i] orelse break`.
brk Break, // break, optionally to a labelled block / loop and/or with a value
cont ?[]u8, // `continue`, optionally to a labelled loop (`continue :outer`)
// `return` / `return v` / `return a, b` — null is a bare void return;
// otherwise the same-line value list (len ≥ 1). A multi-value return is
// statement-position sugar for returning a tuple: `return a, b` lowers to
// `return .{ a, b };`, while a single value lowers verbatim — so
// `return .{ a, b }` (one struct_lit value) round-trips as written.
ret ?[]mut Expr,
}
// A `break`, in its three shapes: a bare loop `break` (both fields null), a
// labelled `break :blk` (label set), and a value-carrying `break :blk v` /
// `break v` (value set). A labelled break targets an enclosing labelled block
// expression (supplying that block's value) or an enclosing labelled loop;
// lowering renders `break`, then ` :label` when labelled, then ` value` when a
// value is present.
pub const Break = struct {
label ?[]u8,
value ?*mut Expr,
}
// `label: { … }` — a block used as an expression. When `label` is set its value
// is whatever a `break :label v` inside it yields; an unlabelled block (`label ==
// null`) is a switch-prong body — a void `=> {}` arm or a multi-statement arm —
// carrying no value. Lowering renders the block body like any other (statements
// at depth + 1, closing brace at depth), prefixed by the label when present.
pub const BlockExpr = struct {
label ?[]u8,
body []mut Stmt,
}
pub const Member = struct {
base *mut Expr,
field []u8,
}
pub const Call = struct {
callee *mut Expr,
args []mut Expr,
}
pub const Index = struct {
base *mut Expr,
index *mut Expr,
}
pub const Slice = struct {
base *mut Expr,
lo *mut Expr,
hi ?*mut Expr, // null == open-ended `[lo..]`
// `[lo..hi :s]` — a sentinel-terminated slice: the result is asserted to end
// at the sentinel value `s`. Independent of `hi` (an open-ended `[lo.. :s]`
// is valid); null == no sentinel, the ordinary slice.
sentinel ?*mut Expr,
}
pub const BuiltinCall = struct {
name []u8, // the bare intrinsic name, no sigil, e.g. "intCast"
args []mut Expr,
}
pub const Unary = struct {
op []u8, // "!", "-", "&", or "~"
operand *mut Expr,
}
// `lhs catch handler` / `lhs catch |name| handler`. The handler runs when
// `lhs` yields an error and is one of two shapes: a recovery *expression* — a
// fallback value or a control transfer (`catch 0`, `catch return e`) — or a
// recovery *block* `catch { … }` (a label-less `block_expr`), the multi-
// statement void-recovery idiom (`device.flush() catch {}`). `capture` is the
// error binding `|name|`, in scope for the handler only; null when unbound.
pub const Catch = struct {
lhs *mut Expr,
capture ?[]u8,
handler *mut Expr,
}
// `asm [volatile] (template : outputs : inputs : clobbers)` — an inline-assembly
// expression, mirroring Zig's (Tier 0 lowers it byte-for-byte). `is_volatile`
// records the `volatile` modifier (every kernel-side use carries it). `template`
// is the instruction text — a string or a `\\` multiline string, lowered
// verbatim. The three operand sections are optional and positional: `outputs`
// and `inputs` are `[name] "constraint" (body)` lists; `clobbers` is the single
// trailing expression (`.{ .memory = true }`), null when absent. The constraint
// strings and the template are an irreducible foreign (LLVM/assembler)
// sublanguage that passes through unchanged — Flash adds no spelling of its own.
pub const AsmExpr = struct {
is_volatile bool,
template *mut Expr,
outputs []mut AsmOperand,
inputs []mut AsmOperand,
clobbers ?*mut Expr,
}
// One asm operand: `[name] "constraint" (body)`. `name` is the bracketed
// symbolic name, `constraint` the verbatim quoted constraint-string lexeme. The
// body is either a return-type output (`-> T`) or an expression operand — an
// lvalue for an output, a value for an input.
pub const AsmOperand = struct {
name []u8,
constraint []u8, // the "…" lexeme, quotes included
body AsmOperandBody,
}
pub const AsmOperandBody = union(enum) {
ret_type TypeRef, // `(-> T)` — a value-producing output
expr Expr, // `(lvalue)` / `(value)`
}
// `if cond thenExpr else elseExpr` — an `if` used for its value (the
// expression form, distinct from the `if` statement). Both arms are
// expressions and both are required: an if-expression always yields a value,
// so there is no else-less form. The condition carries no parentheses (as in
// the statement form); lowering renders the idiomatic Zig `if (cond) a else b`.
pub const IfExpr = struct {
cond *mut Expr,
then *mut Expr,
else_ *mut Expr,
}
// `switch subject { prong, … }` — a switch over a scrutinee. The subject is
// paren-less (like the other control-flow headers); each prong matches it and
// yields a body expression. Lowering renders the idiomatic Zig
// `switch (subject) { … }`.
pub const SwitchExpr = struct {
subject *mut Expr,
prongs []mut SwitchProng,
}
// One switch prong: `patterns => body`, or the default `else => body`. `is_else`
// marks the default (its `patterns` is empty). `capture`, when set, is the
// payload binding a `=> |x|` introduces — the active union variant's payload, in
// scope for `body` only. `body` is any expression — an inline value, an
// `if`-expression, a nested `switch`, or a `{ … }` block (an unlabelled block
// expression) for a void or multi-statement arm.
pub const SwitchProng = struct {
is_else bool,
patterns []mut SwitchPattern,
capture ?[]u8,
// `=> |*x|` — a pointer capture on the payload; same shape as the loop
// flags (`capture` keeps the bare name, the flag carries the `*`).
capture_is_ptr bool = false,
body Expr,
}
// One match item in a prong's pattern list: a single value (`hi == null`) or an
// inclusive range `lo...hi` (`hi` set). Prongs comma-separate their patterns
// (`'\r', '\n' => …`), so a prong holds one or more of these.
pub const SwitchPattern = struct {
lo Expr,
hi ?Expr,
}
pub const Binary = struct {
// The Flash operator lexeme: "+", "-", "*", "/", "%", "==", "!=", "<",
// "<=", ">", ">=", "&&", "||", "orelse". Lowering maps "&&"/"||" to Zig's
// `and`/`or`; the rest pass through unchanged.
op []u8,
lhs *mut Expr,
rhs *mut Expr,
}
// One element of a `.{ … }` literal. A named element is `.name = value` (a
// struct-init field); a positional element leaves `name` null (a tuple element),
// e.g. the single positional field in `.{flibc.sys.dump_free()}`.
pub const StructLitField = struct {
name ?[]u8,
value Expr,
}
// `Type{ .x = 1, … }` / `Type{}` — a type-prefixed initializer (a struct or
// union literal whose type is named rather than inferred from context, unlike
// the anonymous `.{ … }`). `type` is the prefix expression (an identifier, or a
// dotted `pkg.Type`); the fields reuse StructLitField, so the same named /
// positional spelling and zig fmt brace-spacing apply. It lowers verbatim:
// `Type{ .x = 1 }`.
pub const TypedLit = struct {
type *mut Expr,
fields []mut StructLitField,
}
// `struct { name T, …, <decls> }` — a struct type definition, the value of a
// `const Name = struct { … }`. Data fields come first (each `name T`,
// comma-terminated), then any associated declarations — methods (`fn`) and
// constants (`const`) — in source order. The fields-then-decls split mirrors
// the idiomatic Zig layout the lowering emits: the field block, a blank line,
// then the declarations one blank line apart.
pub const StructDef = struct {
// The layout modifier before `struct`: "packed" (bit-exact field packing,
// an on-disk FAT directory entry) or "extern" (C ABI layout, a type that
// crosses a copy_to_user or assembly boundary), emitted verbatim as a
// prefix. Null for an ordinary auto-layout struct. The backing-integer
// form `packed struct(uN)` is not part of the grammar — the field widths
// define the layout.
layout ?[]u8,
fields []mut Field,
decls []mut ContainerDecl,
}
// One associated declaration inside a container body (struct, enum, or union):
// a method, a constant, or an import. All three reuse the top-level node types —
// a method is an ordinary FnDecl whose receiver (when it takes one) is a plain
// first parameter, so there is no implicit `self`; an associated constant is an
// ordinary ConstDecl; an associated import is an ordinary UseDecl (`use
// "syscalls" as sys` lowering to a container-level `const sys =
// @import("syscalls.zig")`), so `use` is the one import spelling at every scope
// instead of an in-container `const … = #import(…)`.
pub const ContainerDecl = union(enum) {
method FnDecl,
constant ConstDecl,
use_import UseDecl,
}
pub const Field = struct {
doc [][]u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
name []u8,
type TypeRef,
// A default field value: `name T = expr` initialises the field when the
// container is constructed with that field omitted (`HistSlot{}`). The
// expression is any value — a literal, `undefined`, an empty `.{}` — lowered
// verbatim after the type (`name: T = expr,`). Null when the field has no
// default (`name T,`).
default ?Expr,
}
// `enum { a, b }` or `enum(u8) { a, b }` — an enum type definition. `tag_type`
// carries the explicit backing integer type of `enum(T)`, null for a bare enum.
// Variants come first, then any associated declarations — methods, constants,
// imports — exactly as in a struct body (the fields-then-decls layout rule).
pub const EnumDef = struct {
tag_type ?[]u8,
variants []mut EnumVariant,
decls []mut ContainerDecl,
}
// One enum variant: a bare name, optionally with an explicit discriminant
// (`perm = 1`). `value` is the discriminant expression, null for an implicit
// variant. Mixing is allowed (`a, b = 5, c`), exactly as Zig permits.
pub const EnumVariant = struct {
doc [][]u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
name []u8,
value ?*mut Expr,
}
// `union(enum) { a, b T, … }` — a tagged-union type definition. `tag` carries
// the verbatim tag selector inside `union(…)`: "enum" for the inferred-tag form
// `union(enum)`, an explicit enum type name for `union(MyTag)`, or null for a
// bare untagged `union`. Variants are name-first like struct fields; a payload
// type is optional (a bare name is a void variant).
// Variants come first, then any associated declarations, as in a struct body.
pub const UnionDef = struct {
tag ?[]u8,
variants []mut UnionVariant,
decls []mut ContainerDecl,
}
// One union variant: a name and an optional payload type. `payload` is null for
// a void variant (the bare `none`), set for a typed variant (`echo u8` →
// `echo: u8`). Mixing is allowed, exactly as Zig permits.
pub const UnionVariant = struct {
doc [][]u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
name []u8,
payload ?TypeRef,
}
// --- tests ---------------------------------------------------------------
// The handwritten ast.zig carries no tests (pure definitions); these smoke
// tests assert the ported shapes stay constructible and tag-addressable from
// Flash, through the generated Zig.
test "an expression node constructs and reads back" {
e := Expr{ .int = "42" }
try sup.expectEqualStrings("42", e.int)
}
test "an item routes by tag" {
it := Item{ .link_decl = .{ .module = "flibc" } }
switch it {
.link_decl => |l| try sup.expectEqualStrings("flibc", l.module),
else => try sup.expect(false),
}
}
test "a bind statement carries its mutability and optional slots" {
s := Stmt{ .bind = .{ .is_mut = false, .is_comptime = false, .name = "x", .type = null, .align_expr = null, .value = Expr{ .int = "1" } } }
try sup.expect(!s.bind.is_mut)
try sup.expect(s.bind.type == null)
}
test "a switch pattern spans an inclusive range" {
pat := SwitchPattern{ .lo = Expr{ .char = "'a'" }, .hi = Expr{ .char = "'z'" } }
try sup.expect(pat.hi != null)
try sup.expectEqualStrings("'a'", pat.lo.char)
}
test "a use declaration distinguishes file imports" {
u := UseDecl{ .is_pub = false, .module = "token", .alias = null, .is_file = true }
try sup.expect(u.is_file)
try sup.expectEqualStrings("token", u.module)
}