ajhahn.de
← Flash
Zig 611 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.

pub const Program = struct {
    items: []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: []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: []const u8, // the "…" lexeme, quotes included
    body: []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).
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: []const []const u8,
    is_pub: bool, // `pub const` / `pub var` — public visibility for an importing module
    is_mut: bool, // `var` — a mutable file-scope binding (`const` is the immutable default)
    name: []const u8,
    type: ?TypeRef,
    value: Expr,
};

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: []const u8,
    alias: ?[]const 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: []const u8, // the string-literal payload
};

pub const FnDecl = struct {
    doc: []const []const 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: []const u8,
    params: []Param,
    ret: ?TypeRef,
    // 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: ?[]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: ?[]const u8, // null == the `_` placeholder
    type: TypeRef,
};

pub const TypeRef = union(enum) {
    name: []const u8, // usize, u8, void, noreturn, argv, cstr, or an imported A.B
    slice: *TypeRef, // []T   (const by default; see lower.zig)
    slice_mut: *TypeRef, // []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: *TypeRef, // [*]T   — a many-item pointer, const pointee by default (see lower.zig)
    many_ptr_mut: *TypeRef, // [*]mut T — a many-item pointer to a mutable pointee
    many_ptr_volatile: *TypeRef, // [*]volatile T — const+volatile pointee (see lower.zig)
    many_ptr_mut_volatile: *TypeRef, // [*]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: *TypeRef, // *T   — a single-item pointer, const pointee by default (see lower.zig)
    ptr_mut: *TypeRef, // *mut T — a single-item pointer to a mutable pointee
    ptr_volatile: *TypeRef, // *volatile T — const+volatile pointee (see lower.zig)
    ptr_mut_volatile: *TypeRef, // *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: *TypeRef, // [_]T   — an array whose length is inferred from its initializer
    array_inferred_sentinel: SentinelPtr, // [_:s]T — its sentinel-terminated form ([_:s]T{ … })
    optional: *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: []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: ?*TypeRef,
    payload: *TypeRef,
};

pub const Array = struct {
    len: *Expr, // the length expression, e.g. 512 or pkg.BUF_LEN
    elem: *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: *Expr,
    sentinel: *Expr,
    elem: *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.
pub const SentinelPtr = struct {
    sentinel: *Expr,
    elem: *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: []TypeRef, // parameter types, in source order; empty == `fn () R`
    ret: ?*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: []const u8,
    args: []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: *Stmt, // `defer <stmt>` — runs the inner statement on scope exit
    errdefer_stmt: *Stmt, // `errdefer <stmt>` — runs it only on an error exit
    defer_block: []Stmt, // `defer { … }` — runs the block on scope exit
    errdefer_block: []Stmt, // `errdefer { … }` — runs it only on an error exit
    expr: Expr, // a bare call, break, continue, return, etc.
};

// `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: []?[]const 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: []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: []const 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: []const 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: ?[]const u8,
    body: []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: ?[]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: ?[]const u8,
};

pub const While = struct {
    is_inline: bool, // `inline while` — a compile-time-unrolled loop (lowered with an `inline ` prefix)
    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: ?[]const u8,
    body: []Stmt,
    // The loop `else` arm: runs when the loop ends without `break` (condition
    // false / payload exhausted), lowering to Zig's `while (…) … else { … }`.
    else_body: ?[]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: ?[]const 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)
    captures: [][]const u8,
    iter: Expr,
    range_hi: ?Expr, // set → `iter` is a range's low bound and this its high bound
    body: []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: ?[]Stmt,
};

pub const Expr = union(enum) {
    int: []const u8, // decimal, 0x hex, 0o octal, or 0b binary integer; verbatim
    float: []const u8, // decimal float, e.g. 3.14 or 1.5e-3; verbatim
    string: []const 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: [][]const u8,
    char: []const u8, // 'c' (lexeme including quotes)
    ident: []const 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: []const u8,
    member: Member, // expr.field
    deref: *Expr, // expr.* — single-item pointer dereference (also a valid lvalue)
    optional_unwrap: *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: []StructLitField, // .{ a, b } / .{ .x = 1 } — empty slice == .{}
    typed_lit: TypedLit, // Name{ .x = 1 } / Name{} — a type-prefixed initializer
    type_lit: *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: []const u8, // .red — an inferred enum literal (the bare variant name)
    error_lit: []const u8, // error.Name — an error-value origination (the bare error name)
    error_set: [][]const 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: *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: *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 and/or with a value
    cont, // continue
    // `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: ?[]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 block
// expression and supplies that block's value; lowering renders `break`,
// then ` :label` when labelled, then ` value` when a value is present.
pub const Break = struct {
    label: ?[]const u8,
    value: ?*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: ?[]const u8,
    body: []Stmt,
};

pub const Member = struct {
    base: *Expr,
    field: []const u8,
};

pub const Call = struct {
    callee: *Expr,
    args: []Expr,
};

pub const Index = struct {
    base: *Expr,
    index: *Expr,
};

pub const Slice = struct {
    base: *Expr,
    lo: *Expr,
    hi: ?*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: ?*Expr,
};

pub const BuiltinCall = struct {
    name: []const u8, // the bare intrinsic name, no sigil, e.g. "intCast"
    args: []Expr,
};

pub const Unary = struct {
    op: []const u8, // "!", "-", "&", or "~"
    operand: *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: *Expr,
    capture: ?[]const u8,
    handler: *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: *Expr,
    outputs: []AsmOperand,
    inputs: []AsmOperand,
    clobbers: ?*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: []const u8,
    constraint: []const 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: *Expr,
    then: *Expr,
    else_: *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: *Expr,
    prongs: []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: []SwitchPattern,
    capture: ?[]const u8,
    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: []const u8,
    lhs: *Expr,
    rhs: *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: ?[]const 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: *Expr,
    fields: []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 {
    fields: []Field,
    decls: []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: []const []const u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
    name: []const 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: ?[]const u8,
    variants: []EnumVariant,
    decls: []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: []const []const u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
    name: []const u8,
    value: ?*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: ?[]const u8,
    variants: []UnionVariant,
    decls: []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: []const []const u8, // leading `///` doc lines, empty == none (see ConstDecl.doc)
    name: []const u8,
    payload: ?TypeRef,
};