Flash

Changelog

Every notable change, release by release.

README · Vision · Tutorial · Reference · Cookbook · Setup · Versioning · Changelog · License

--- All notable changes to Flash are recorded in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.6.1] - 2026-06-12 ### Added - **GitHub renders `.flash` as Zig.** A new `.gitattributes` maps `*.flash` to Zig for linguist — the closest registered grammar to Flash's surface — so Flash sources get syntax highlighting on GitHub instead of plain text. Native recognition waits until Flash qualifies for github-linguist's registry. - **Shell helpers catch up with the self-hosted toolchain.** The day-to-day verbs in `flash.env.zsh` (`run`, `fmt`, `tokens`, and the new `emit` and `tree`) now go through `flashc-stage1` — the live, self-hosted compiler — instead of the frozen stage0 seed, which rejects post-v0.5 grammar. New verbs: `flash gates` runs the full pre-commit gate (test + fixpoint + diff-corpus), `flash test ` reaches the individual suites, `flash lsp` builds flashd, and `flash version` prints the compiler version. Sourcing the file now warns when the installed Zig disagrees with `.zigversion`, build errors are no longer swallowed, file arguments work from any directory, and the verbs tab-complete. - **A cookbook.** `COOKBOOK.md` collects the patterns that make Flash code pleasant to write — one arena per job, the three error-handling spellings, `#ptrCast`-outermost cast chains, bit manipulation, struct literals and formatter habits, JSON in a few lines. Every fenced example is real compiled code: the recipes live verbatim in `examples/register/cookbook.flash`, built by the compiler's own gates and exercised by `test` blocks, so a snippet cannot quietly rot. Linked from the header of every documentation page and from the README. - **A build driver (`-o`, `flashc build`).** `flashc file.flash -o out.zig` writes the lowered Zig to a file instead of stdout, and `flashc build ` transpiles every `.flash` file under a source tree into a mirrored tree of `.zig` twins, stopping with a non-zero exit on the first diagnostic. Deliberately not a build system: compiling the emitted Zig stays with `zig build` — SETUP.md's new "Using Flash in your own project" section shows the two wired together, and `--anchors` / `--plain-diagnostics` compose with both new modes. A new harness (`zig build test-driver`, part of `zig build test`) pins the written bytes, exit codes, and flag composition. - **Source anchors in the emitted Zig (`--anchors`).** `flashc --anchors file.flash` prefixes every lowered top-level constant and function with a `// :` comment naming the Flash line it was lowered from, so an error the Zig toolchain reports inside the generated file traces back to its Flash origin by eye. The comment carries the file's basename only — never the invoking directory — and sits above any doc comment, so `///` blocks stay attached to their declaration. Strictly opt-in: without the flag, emission is byte-identical to before (the differential corpus proves it). - **Caret diagnostics.** `flashc` now prints the offending source line under every parse and semantic diagnostic (notes included), with a `^` caret at the anchored column and a `~` tail covering the rest of the anchored span. Parse diagnostics now carry the offending token, so they point at it too. The previous bare one-line form stays available via `--plain-diagnostics`, accepted in any argument position. - **Format-on-save editor recipes.** `editors/nvim/README.md` gains a ready-to-paste `BufWritePre` autocmd that routes the buffer through `flashc fmt` before every write, and `editors/vscode/README.md` gains a `tasks.json` entry plus a "Run on Save" wiring — so a `.flash` file is written in canonical form from the start. A file with a parse error is left untouched in both setups (the formatter refuses rather than destroys). - **Editor wiring for `flashd`.** `editors/nvim/README.md` gains the ready-to-paste `vim.lsp.start` autocmd for Neovim's built-in LSP client — open a `.flash` file with an error and the squiggle appears, fix it and it clears (verified against a live Neovim) — and `editors/vscode/README.md` notes how to reach the server until a dedicated client extension exists. `SETUP.md` documents the `test-lsp` and `lsp` build targets. - **`flashd` publishes live diagnostics.** The server now runs the self-hosted compiler frontend in-process over every open document — the lexer and parser on each open and change, the semantic checker added on save — and publishes `textDocument/publishDiagnostics`: parse errors underline their whole line (the parser's diagnostic carries no column), sema findings get a point range from their source anchor, and a diagnostic's note becomes a second information-severity entry at its own location. Every run publishes a complete list, empty included, so stale squiggles clear on the first clean parse and on close. No flashc process is spawned and no error text is parsed back — the diagnostics structs feed the wire format directly (`tools/lsp/check.flash`). - **`flashd` — the Flash language server core (`zig build lsp`).** The server now speaks JSON-RPC end to end: `tools/lsp/server.flash` is the pure dispatch core — the initialize lifecycle with capability negotiation (`textDocumentSync: Full`, `positionEncoding: "utf-8"` when the client offers it), the shutdown/exit handshake with honest exit codes, the full-sync document store, MethodNotFound / ServerNotInitialized / ParseError answers — and `tools/lsp/main.flash` is the stdio driver: protocol on stdout only, logs on stderr, one arena per message so an hours-long session accumulates nothing. `zig build lsp` installs the `flashd` binary. The standard library's `List(T)` gains `swapRemove` for the store. - **`tools/lsp/transport.flash` — LSP base-protocol framing.** The first module of the Flash language server, a standalone Flash application transpiled by the self-hosted compiler (it is not part of the bootstrap, so it may use the full current surface). `scan` recognizes one `Content-Length`-framed message in a byte buffer, `frame` wraps a body for writing, and `Decoder` accumulates arbitrary read chunks and hands out complete bodies in order — all pure functions over slices, tested by feeding byte buffers one byte at a time. New build target: `zig build test-lsp` (also wired into `zig build test`). - **`std/json.flash` — JSON in the standard library (`core.json`).** A minimal RFC 8259 value tree: `parse` into `null`/`bool`/`int`/`number`/`string`/`array`/`object` nodes and `stringify` back out, with strict rejection of malformed input (trailing data, trailing commas, lone surrogates, leading zeros, unescaped control characters) and full string-escape decoding including `\uXXXX` surrogate pairs. Integers parse to `i64`; fractions, exponents, and out-of-range integers keep their raw lexeme, so a round trip never loses digits. Arena-oriented allocation contract, a 128-level nesting guard, and failing-allocator sweeps in the test suite. First consumer: the Flash language server. - **`REFERENCE.md` — the Flash Language Reference.** A single public document covering the whole v0.6 surface: lexical structure, the type system with its Zig lowerings, declarations, functions, statements, expressions with the implemented precedence table, builtins, comptime, error handling, containers, modules, inline assembly, test blocks, the formatter, and a Zig-delta chapter — every section with compilable examples, checked against the self-hosted compiler (the normative EBNF in `selfhost/parser.flash`) by probe programs run through stage1. Linked from every public doc's navigation line and shipped in the package `.paths`. ### Fixed - **The tutorial is usable on phones.** The chapter sidebar — previously a fixed block that crowded small screens — is now an off-canvas drawer behind a header hamburger, and the app tracks the real mobile viewport (`100dvh`) so the Transpile button and console stay reachable when the browser chrome collapses. The compiler console collapses on tap and auto-expands on errors, the editor uses a 16px font on phones to stop iOS focus-zoom, and loading an example opens the full-height editor view. Also repairs a CSS class mismatch (`.terminal-body` vs. `.terminal-output`) that had left the console unstyled (`tutorial/public/`). ## [0.6.0] - 2026-06-12 ### Added - **`std/fmt.bufPrint` and `std/math.maxInt` — formatting without an allocator, bounds from both ends.** `bufPrint` formats into a caller buffer and returns the written prefix — the same comptime-checked format engine as `allocPrint`, now shared through an internal sink seam, with a too-small buffer reporting `NoSpaceLeft` — so allocation-free print paths need no allocator at all. `maxInt` completes `minInt`: the largest value of any integer type as a `comptime_int`, exact at every width. - **`std/mem` — endian codecs and byte views.** `readInt` / `writeInt` read and write unsigned integers through an explicit byte order (`mem.Endian`, `.little` / `.big`), `asBytes` / `asBytesMut` view any value's memory as a byte slice — split into a const and a mutable verb because Flash spells pointee mutability in the type — and `sliceTo`, `span`, and `alignForward` round out the slice toolkit: cut at the first matching element, count a 0-terminated pointer into a slice, and round an address up to a power-of-two boundary. All pure Flash, each with in-module tests that double as usage examples. - **`std/README.md` — the standard library gets its front door.** A module-level index of the `core` library: what each module provides, the confinement rule that keeps `base` as the only Zig door, and the documentation convention made explicit — each module's header comment block is its reference, and the in-module `test` blocks double as usage examples. One line per module by design, so the index stays honest as functions grow inside the existing files. - **Failing-allocator sweep — every allocation-failure path in std is now proven.** The standard library's allocating surface — `List` growth, `appendSlice`, `toOwnedSlice`, `fmt.allocPrint`, and the arena's chunk chain — now runs under an exhaustive induced-failure harness: each test body is re-run with every one of its allocations made to fail in turn, and every induced out-of-memory must surface as an error with nothing leaked. Targeted tests pin the recovery contracts directly: a failed `List` grow leaves the list untouched and still usable, a failed first chunk leaves the arena empty, and a failed chunk *chain* releases the partially built chunk and keeps the live one serving — the arena's cleanup path is now proven, not just read. The base shim gains the two test-only hooks this requires (`FailingAllocator`, `checkAllAllocationFailures`), enumerated like the rest of its surface. - **`ArenaAllocator` — the arena allocator, in pure Flash.** `std/arena.flash` lands the allocator that releases everything in one `deinit`: chunks of backing memory from a child allocator chained in a list, allocation bumping a cursor inside the newest chunk, and the handle it returns is a real `Allocator` built from a Flash-implemented v-table — `alloc`, `resize`, `remap`, and `free` are Flash functions behind Zig's standard allocator interface, so anything that takes an `Allocator` works through the arena unchanged. The observable contract mirrors Zig's arena: `free` rewinds only the most recent allocation, `resize` grows in place only at the end of the newest chunk, and everything else is reclaimed at `deinit`. The self-hosted compiler now runs on it — its support facade re-routes `ArenaAllocator` to `core.arena`, putting every arena-backed test harness in the pipeline through the Flash implementation, under the full test suite and the bootstrap fixpoint. This completes the standard library's v0.6 foundation: memory, containers, formatting, parsing, and allocation are pure Flash. - **`fmt` and `math` — formatting, parsing, and integer bounds, in pure Flash.** `std/fmt.flash` lands `allocPrint` — the format string is walked at comptime, so an unknown verb or a mismatched argument count is a compile error — and `parseInt`, which mirrors Zig's exactly: optional leading sign, base 0 auto-detects a `0b`/`0o`/`0x` prefix, `_` digit separators are legal only between digits, and accumulation is overflow-checked on the negative side so a type's own minimum parses cleanly. `std/math.flash` seeds the math module with `minInt`, computed from the type's bit width at comptime. The self-hosted compiler now runs on all three: every diagnostic message it formats and every integer literal it folds flows through the Flash implementations, under the full test suite and the bootstrap fixpoint. - **`List(T)` — the standard library's dynamic array, in pure Flash.** `std/list.flash` lands the library's workhorse container as a generic function returning a struct: `.empty` to initialize (no allocation until the first append), `append`, `appendSlice`, `toOwnedSlice`, `deinit`, and the `.items` slice for direct indexing — the same unmanaged surface the Zig-backed list exposed, so the swap touches no call site. The allocation is carried as a second slice rather than a raw capacity count, growth doubles capacity (floor 4) and jumps straight to the requested length on bulk appends, and on allocation failure the list is untouched. The self-hosted compiler now runs on it: its support facade re-routes `List` to `core.list.List`, putting every token, AST node, and diagnostic the compiler accumulates — about twenty-five element-type instantiations — through the Flash implementation, under the full test suite and the bootstrap fixpoint. - **The standard library is born: `std/`, the `core` module.** Flash now ships the seed of its own standard library, written in Flash. `std/core.flash` is the root (re-exports only), `std/base.flash` is the enumerated Zig interop shim (the only std file that imports Zig's std — the same confinement rule the compiler's support shim follows), and `std/mem.flash` lands the first real module: `eql`, `indexOf`, `indexOfScalar`, `copy`, `set`, `lessThan`, and a stable insertion `sort` (context + comparator), all generic, all pure Flash. The self-hosted compiler now consumes them — its support facade re-routes the slice verbs to `core.mem` with the facade surface unchanged, so the whole compiler test suite and the bootstrap fixpoint run through the new code paths. New build step `zig build test-std` runs the library's own Flash test blocks (wired into `zig build test`); the std sources join both differential corpora, which structurally enforces that the frozen bootstrap seed can always compile them. - **`errdefer |err|` capture.** The deferred cleanup now sees which error is unwinding: `errdefer |err| log(err)` (statement form) and `errdefer |err| { … }` (block form) bind the error for the deferred code only, by value — `errdefer |*err|` is rejected like every other error capture, and a capture pipe on plain `defer` gets its own targeted diagnostic (there is no error on a normal exit). The capture scopes like the `catch` capture (it does not leak past the statement; an unused capture is rejected), lowers verbatim into Zig's slot, and format-round-trips (a spaced `| err |` canonicalises). New example `examples/register/errdefer_capture.flash`. Editor grammars: no change (`errdefer` was already a keyword). - **`align(N)` in pointer/slice types and on file-scope bindings.** The alignment qualifier now rides a pointer or slice type — `[]align(16) u8`, `*align(4) mut volatile T`, `[*:0]align(2) mut u8` — directly after the prefix, before `mut`/`volatile` (Zig's slot; a misplaced or array-position `align` gets a targeted diagnostic naming the canonical spelling). The bind-position form, previously local-only, now also parses at file scope — `var pool [N]u8 align(4096) = undefined`, with or without a type, on `extern var` too — between the type and any `linksection`. The alignment is part of a pointer type's identity (as in Zig), lowers verbatim into Zig's slot (`[]align(16) const u8`), and format-round-trips (a spaced `align ( 16 )` canonicalises). New example `examples/register/aligned_types.flash`; `examples/register/repetition.flash` regains the alignment its source kernel buffer carries. Editor grammars: no change (`align` was already a keyword). - **Labeled loops.** A label on `while` / `for` is a break/continue target: `outer: while … { break :outer }` leaves the labeled loop from anywhere inside it, and `continue :outer` restarts its next iteration — unifying loop labels with the labeled block's `break :label v` that already existed (one label grammar, loop or block). The label precedes `inline` (`outer: inline while`), lowers verbatim to Zig's prefix, and format-round-trips (a spaced `outer : while` canonicalises). The checker resolves every labelled break/continue lexically: an unknown label, a `continue` aimed at a block label, and a label nothing targets each get a targeted diagnostic; a loop's label is not visible from its `else` arm (matching Zig). New example `examples/register/labeled_loop.flash`. Editor grammars: no change (a label is a plain identifier, the labeled-block precedent). - **`|*x|` pointer captures.** A capture binds a pointer to the element or payload instead of a copy, so the body writes in place: `for *p in &arr { p.* = 0 }` lowers to Zig's `for (&arr) |*p|`, and the same `*` works on the `if`/`while` payload captures (`if opt |*x|`, `while it.next() |*v|`) and on switch prongs (`.variant => |*pay|`) — everywhere Zig permits a pointer capture. The `*` rides the element capture only: an index capture stays a value, a `*_` discard is rejected (there is no address worth taking), and the error captures (`catch |e|`, `else |e|`) still bind by value — each with a targeted diagnostic. The pointer itself remains an immutable capture (`p = q` is rejected; `p.* = v` is the point). Lowers verbatim, format-round-trips, and a spaced `| * x |` canonicalises to `|*x|`. New example `examples/register/pointer_capture.flash`. Editor grammars: no change (no rule keys on the capture shape). - **`callconv` in function types.** A function type takes an explicit calling convention between its parameter list and return type: `?*fn(u32, *mut [512]u8) callconv(.c) i32` spells an optional C-ABI function pointer as a field type — the v-table shape, where a driver table's entries cross an ABI boundary and one field can carry a default stub. The convention sits exactly where a declaration signature puts it (Zig's slot order), lowers verbatim, and format-round-trips; the convention is part of the type's identity at compile time, so `fn() callconv(.c)` and `fn()` are distinct types. The inferred-error-set rejection on function-type returns still fires past a `callconv(…)`. New example `examples/register/callconv_fn_type.flash`. Editor grammars already keyword `callconv` — no grammar change. - **`packed struct` / `extern struct` layout modifiers.** A struct definition takes an optional layout modifier: `packed struct { … }` packs the fields bit-exactly — an on-disk format whose byte offsets are pinned by comptime asserts — and `extern struct { … }` lays the fields out by the C ABI, for a type that crosses an ABI boundary and must look identical on both sides. The modifier prefixes the keyword exactly as in Zig, lowers verbatim, and format-round-trips; field defaults and associated declarations work unchanged under either layout. The backing-integer form `packed struct(uN)` is not part of the grammar — the field widths define the layout, and a targeted diagnostic says so; a modifier on any other container (`packed union`, `extern enum`) is likewise rejected on the `.flash` line. New example `examples/register/packed_extern_struct.flash`. Editor grammars keyword `packed` in lockstep. - **`linksection("…")` declaration attribute.** Place a symbol in a named section: `var scratch [N]u8 linksection(".sdscratch") = undefined` pins a buffer into its own NOLOAD region, `const PAD [4096]u8 linksection(".rodata") = …` pins read-only data where the linker script expects it. The attribute sits between the type and `=` on a file-scope binding, and between the parameter list and any `callconv(…)` on a function — Zig's slot order in both positions; the inner expression lowers verbatim and format-round-trips. Two targeted rejections: an `extern var` cannot carry one (the defining object owns the section), and a local binding cannot either (a stack slot lives in no section). New example `examples/register/linksection.flash`. Editor grammars updated in lockstep. - **`export var` / `extern var` declarations.** The function-modifier matrix extends to file-scope `var`: `export var nr_tasks i32 = 0` defines a cross-object symbol, and `extern var nr_tasks i32` consumes one — typed, with no initializer (the storage is defined elsewhere, exactly like an `extern fn` prototype is bodyless). `pub` composes in Zig's order (`pub export var`, `pub extern var`). The parser rejects an `extern var` carrying an initializer or missing its type with targeted diagnostics; the checker treats both forms as ordinary mutable globals. Lowers verbatim and format-round-trips. This is the kernel symbol-sharing pattern — one module owns the storage, every other object names it — and covers linker-script symbols (`extern var _kernel_pa_end u8`; the address is the value). New example `examples/register/export_extern_var.flash`. - **Array repetition — the `**` operator.** `[_]u8{0} ** SIZE` builds an array by repeating a comptime-known operand, exactly as in Zig: the canonical zero-init for fixed buffers, lookup tables, and pointer slots. `**` sits in the multiplicative precedence tier (so `a ++ b ** c` reads `a ++ (b ** c)`), lexes by maximal munch (a spaced `* *` stays two stars), lowers verbatim, and format-round-trips. Works in struct-field defaults, with anonymous-tuple operands (`.{0} ** 32`), typed non-`u8` elements, and parenthesized count expressions. New example `examples/register/repetition.flash`. Editor grammars updated in lockstep. - **Wrapping compound assignment — `+%=`, `-%=`, `*%=`.** The assignment forms of the wrapping binops: `head +%= 1` stores the wrapping sum back into the target, completing the `+%`/`-%`/`*%` family the operator tier already carried. All three forms lex as one three-byte token (maximal munch — a spaced `+% =` stays a binop and a store), parse as ordinary assignments (member and index targets included, immutability checked), lower verbatim to Zig's identical spelling, and format-round-trip. New example `examples/register/wrapping.flash` — the first sample in `examples/register/`, home of post-v0.5 grammar that the frozen stage0 bootstrap compiler cannot parse; the directory joins the `fixpoint` corpus while the stage0-facing suites stay on `examples/`. Editor grammars (VS Code, Neovim) updated in lockstep. ### Fixed - **Empty container definitions now lower to zig fmt's one-line form.** A `struct`, `enum`, or `union` definition with no fields, variants, or declarations emitted as `struct {\n};` — two lines where zig fmt wants the one-line `struct {};` — so an empty marker type tripped any `zig fmt --check` gate over the generated code. All three container kinds (including tagged and layout-qualified forms like `union(enum)` and `packed struct`) now emit the one-line `{}` in both the bootstrap and the self-hosted compiler; any member at all keeps the multi-line layout, unchanged. - **`||` on error sets is now rejected at the Flash line.** `AError || BError` parsed as the boolean or it is and lowered to invalid Zig (`AError or BError`), so the mistake only surfaced downstream as a confusing Zig error. The compile-time evaluator now interns an `error { … }` definition as the nominal type it is — like every other container — and a `||` whose operand is a type reports `'||' is boolean or and cannot merge error sets — declare the combined error set explicitly`, anchored at the expression, in both the bootstrap and the self-hosted compiler. Error-set merge stays out of the language by design; the diagnostic names the alternative. - **`flashc fmt` — a block-bodied switch prong no longer adopts a later prong's comments.** The formatter bounded a prong body's closing-brace comment flush by the offset just past the *whole switch*, so a `{ … }` prong could pull a later prong's comments — standalone lines and trailing comments alike — back to the end of its own body, and a leading comment inside a prong could be re-sited out of it. The switch emitter now narrows that bound to the next prong's anchor (mirroring the per-statement narrowing inside blocks), in both the bootstrap and the self-hosted formatter, so every comment stays in the prong that owns it. Regression tests cover all three drift shapes; the formatting corpus is byte-unchanged. ## [0.5.0] - 2026-06-11 ### Added - **The bootstrap closes — `zig build stage2` and the `fixpoint` gate.** `stage2` rebuilds the compiler from stage1-emitted Zig: the same composed-directory mechanism as stage1, with the stage1 binary as the transpiler — the self-hosted compiler compiling its own sources. `zig build fixpoint` is the one-command certification of the result: stage1 and stage2 each transpile every selfhost module and every example twice, and the gate asserts that every run transpiles cleanly, that each binary emits identical bytes across its two runs (determinism), and that stage1 and stage2 emit byte-identical Zig (the fixpoint). When all three hold the generation chain is closed — a further stage would be compiled from the very bytes that built stage2. The gate is green at 32 corpus sources, alongside the full differential corpus (45 files × 3 modes, zero differences) and the unchanged host and selfhost suites. With every module swapped and the handwritten compiler frozen as the bootstrap seed, the build graph also sheds its staged-module scaffolding: the stage1 composition now transpiles all ten selfhost modules unconditionally. - **Self-hosted driver — stage1 is now 100% Flash-origin.** The CLI driver lands as `selfhost/main.flash` and swaps into the stage1 build: argument parsing, `--version`/`--help`/`--dump-tokens`, the `fmt [--check]` subcommand, the full transpile pipeline drive, and diagnostic printing with source-ordered error/note reporting and the established exit codes. With this ninth swap every module of the stage1 compiler — token, lexer, AST, parser, sema, eval, lower, fmt, and the driver — is produced from Flash source; the handwritten Zig compiler under `src/` is now the frozen bootstrap seed (bugfixes only, no features), kept so a clean clone still builds with nothing but a Zig toolchain. The gate is the differential corpus — 45 files across transpile, token-dump, and format modes with zero differences, rejection diagnostics included — plus byte-identical transpilation of all 22 examples, format idempotence through the stage1 binary, and a command-by-command CLI parity sweep over the help, version, missing- file, and missing-argument paths (identical output and exit codes, stage0 vs stage1). - **Self-hosted formatter — `flashc fmt` is now Flash-authored.** The canonical-layout renderer lands whole as `selfhost/fmt.flash` and swaps into the stage1 build: the comment-reattachment machinery (standalone, trailing, and block-close placement with the offset-boundary and indentation-column rules), blank-line preservation, the `:=` short-declaration canon and its destructure extension, doc-comment blocks, and the full item/statement/expression/type rendering surface, mirror of the lowering's. The gate is the formatter's own three guarantees plus the differential corpus: `fmt` output byte-identical to the handwritten formatter across all 44 corpus files (zero differences across transpile, token-dump, and format modes, rejection diagnostics included), reformatting idempotent over every example through the stage1 binary, and the ported test suite — 32 fixtures covering round-trip stability, lowering invariance, and comment-multiset preservation — green against the same sources the handwritten tests used. Seven of the eight compiler modules are now Flash-origin; only the CLI driver remains handwritten. - **Self-hosted lowering complete — the emitter swaps in.** The stage1 compiler now lowers through `selfhost/lower.flash`: the generated Zig replaces the handwritten emitter in the hybrid build, so every byte of Zig that stage1 emits is produced by Flash-authored code. The gate is the milestone's central equality — all 22 examples transpile through stage1 byte-identical to the handwritten compiler's output, and the differential corpus (43 files across transpile, token-dump, and format modes, rejection diagnostics included) shows zero differences. Before the swap, the test set grew by 24 fixtures ported from the handwritten suite — the full set of program ports (among them cat, ls, tokenize, readfile, heap, io, keys, pager, start, and the libc surface) plus the function-pointer v-table shape, composite type aliases, doc-comment placement, test blocks, reserved value keywords, and pointer dereference — each asserting byte-identical output against the handwritten emitter over whole-program sources. - **Self-hosted lowering, second half — statements and expressions.** `selfhost/lower.flash` completes the emitter's rendering surface. The statement set lands whole: discards, bindings with their `comptime` and `align` qualifiers, plain and compound assignment, destructuring binds and assigns, `if` with idiomatic else-if chains and payload / else-error captures, `while` and `for` with their inline forms, range and indexed captures and loop `else` arms, `defer`/`errdefer` in statement and block form, and the block-form statement rule (a bare `switch` or labeled block takes no terminator). So does the full expression matrix: literals including multiline strings, member/dereference/unwrap/call/index postfix chains, slicing with the bound-spacing and sentinel layout rules, builtin calls, unary and binary operators with the `&&`/`||` → `and`/`or` mapping and the wrapping forms, anonymous and typed initializers under the brace-spacing rule, error origination and error sets, the value `if`, `switch` with value lists, inclusive ranges, payload captures and block arms, labeled blocks with valued `break`, `try` and `catch` with captured block recovery, the multi-return tuple sugar, and inline assembly in both the single-line and multi-line layouts with positional operand sections. The staged module still compiles and tests alongside the handwritten emitter; 35 new tests assert byte-identical output against it over statement- and expression-heavy sources, the handwritten suite's own fixtures. - **Self-hosted lowering, first half — declarations and the type surface.** `selfhost/lower.flash` begins the port of the largest compiler module, the AST-to-Zig emitter. This first half renders the declaration surface — imports and their aliases, `link` folding into one `comptime` block, top-level constants and mutable globals, function signatures with the full modifier matrix (`pub`/`export`/`extern`/`inline`, explicit `callconv`, `comptime` and `_` parameters, the void-return default), struct/enum/union definitions with doc comments, field defaults, discriminants, payloads and associated declarations, multiline-string constants, and `comptime`/`test` blocks — plus the complete type-rendering matrix: the whole pointer family with its const-by-default and `volatile` spellings, sentinel forms, arrays (fixed, inferred, sentinel-terminated), optionals, error unions, function types and pointers, generic applications, tuples, and the `argv`/`cstr` builtin aliases with their shadowing rule. Statement and expression rendering is the module's second half and fails loudly until it lands; the staged module compiles and tests alongside the handwritten emitter without displacing it. Every test asserts byte-identical output against the handwritten emitter over the same source. - **Self-hosted compile-time evaluator — checker and evaluator swapped in together.** `selfhost/eval.flash` ports the evaluator whole: the value/type pool (well-known entries at fixed leading indices, structural keys for the optional/pointer/array/error-union/function/tuple composites, nominal container identity by defining AST node, generic instances keyed on the owning declaration plus argument identities — interning as the instantiate-once memoization), and the walking pass with its three-valued discipline: known values fold (integer arithmetic, comparisons, boolean logic, bitwise operators and shifts, string and builtin-type interning, aliases, dead-arm pruning under known conditions), definite errors are reported at the Flash source line (division or remainder by a known zero, generic arity and argument-kind violations, instance result typing, the recursion depth cap and instantiation budget), and everything out of reach degrades to a silent `unknown` — never a false diagnostic, never a change to the emitted Zig. `sema.flash` gains the evaluator driver: `check` runs the evaluator after the binding passes and folds its diagnostics into the one collected list, exactly as the handwritten checker does. With checker and evaluator both whole, the pair is **swapped**: `flashc-stage1` now compiles generated `sema.zig` and `eval.zig` in place of the handwritten ones — six of eight compiler modules are Flash-origin in the hybrid build — and the corpus differential (every example, selfhost source, and checker/evaluator probe, including each rejection's diagnostics and exit status) is byte-identical between the two compilers. The support shim gains two re-exports (`parseInt`, `minInt`). - **Self-hosted semantic checker.** `selfhost/sema.flash` ports the binding/scope/mutability checker whole: the single scope stack with its frame discipline, member-access root resolution (innermost-member resolution, no double reports), the mutability check on bare-identifier assignment targets (single and destructuring), the unused-binding check with its underscore and extern-prototype exemptions, the ignored-value check for the statement-split hazard, redeclaration and shadowing (forbidden outright, with the prior-declaration note), the type-position and container-shape use marking that keeps type-only bindings from false unused flags, container-decl descent into method bodies, and `locate` — line and column recovered from a diagnostic anchor's address, the slice-into-source invariant the AST guarantees. Every diagnostic string is verbatim from the handwritten checker. The module is staged, not swapped: the hybrid build still compiles the handwritten checker, while the port's generated Zig builds and runs its own test suite — the ported checker tests plus the clean-path constant-folding and generic-application probes (the evaluator-driven diagnostics join with the evaluator's own port, which is when the pair swaps in together). The support shim gains one re-export (`assert`). - **Self-hosted parser, complete — fourth module swap.** `selfhost/parser.flash` now ports the parser's second half — the whole statement and expression grammar: blocks and statement dispatch, bindings (`:=`, `var`/`const`, `comptime`, `align(…)`), destructuring binds and assignments with their targeted rejections, `if`/`while`/`for` with captures, loop `else` arms and the capture-pipe disambiguation, `defer`/`errdefer` (statement and block forms), `switch` with value lists, inclusive ranges, payload captures and block bodies, inline assembly, error originations and sets, the precedence climb (with `catch` handlers and the wrapping/concat operator tiers), the postfix chain (member, deref, unwrap, call, index, slice-with-sentinel, typed literals under the control-header suppression rule), anonymous and typed literals, container definitions (struct/enum/union with fields, variants, methods, constants, associated imports), value `if` expressions with the required-parens rule, labeled blocks, multi-return, and the multiline-string fold — every diagnostic string verbatim. The two loud stubs at the old split line are gone, and the module is swapped: `flashc-stage1` now compiles generated `parser.zig` in place of the handwritten one, so all four ported modules (token, lexer, AST, parser) are Flash-origin in the hybrid build. The corpus differential is the license: every corpus file — examples, the self-hosted compiler sources, the checker probes, and the rejection probes' diagnostics — parses through the Flash parser byte-identically in all three modes. 66 new Flash `test` blocks port the remaining handwritten parser tests (statements, expressions, containers, rejections) and add fold and grouping probes; the stub-contract test retires with the stubs. - **Self-hosted parser, first half.** `selfhost/parser.flash` ports the declaration side of the parser (`src/parser.zig`) to Flash: the token cursor and lookahead helpers, every top-level item (`use` / `link` / `const` / `var` / `fn` with its modifier matrix and `callconv`, `comptime` blocks, `test` blocks, doc-comment attachment and its targeted rejections), and the complete type grammar — slices, the pointer/sentinel/volatile matrix, arrays, optionals, infix and prefix error unions, function types, tuple types, and generic application — with the guiding diagnostics carried over verbatim. The statement and expression grammar is the module's second half; until it lands the module is **not** swapped into `flashc-stage1`: its generated Zig joins the composed build under a staged name, where 36 ported-and-new Flash `test` blocks compile against the self-hosted token/lexer/AST modules and run under `zig build test`, and the file joins the `diff-corpus` equality gate as an input. The deliberate stubs at the split line fail loudly (a dedicated test pins that) rather than misparse. - **Self-hosted AST.** `selfhost/ast.flash` ports the AST node definitions (`src/ast.zig` — the program/item/statement/expression/type-reference shapes, every string field a borrowed slice into the source buffer) to Flash, and `flashc-stage1` now compiles the generated module in place of the handwritten one — the third module swap. The ported declarations are byte-identical to the handwritten ones (comment-stripped diff: zero lines), so the remaining handwritten pipeline stages compile against the generated types unchanged, and `zig build diff-corpus` holds the swapped compiler byte-identical over the whole corpus. Smoke tests assert the shapes stay constructible and tag-addressable from Flash. - **Self-hosted lexer.** `selfhost/lexer.flash` ports the lexer (`src/lexer.zig` — a single forward pass, zero allocation, tokens as byte spans into the caller's source buffer) to Flash, and `flashc-stage1` now compiles the generated module in place of the handwritten one — the second module swap through the hybrid pipeline, and the first whose logic runs on every compile. All 35 of the handwritten lexer's tests ride along as Flash `test` blocks (keyword, operator, literal, and comment shapes; maximal munch; the literal-adjacency guard), and `zig build diff-corpus` proves the swapped compiler byte-identical over the whole corpus — the `--dump-tokens` token stream included, which now flows through the Flash-authored lexer. - **First self-hosted compiler module.** `selfhost/token.flash` ports the token taxonomy (`src/token.zig` — the `Kind` enum, the `Token` span, and keyword resolution) to Flash, and `flashc-stage1` now compiles the generated module *in place of* the handwritten one — the first real swap through the hybrid pipeline. Keyword lookup is a flat linear scan over the 41 reserved words (the set is frozen with the v1 grammar; an exhaustive test asserts every word maps to its kind), and the module's Flash `test` blocks run under `zig build test-selfhost`. `zig build diff-corpus` proves the swapped compiler byte-identical to the handwritten one over the whole corpus. - **Self-host scaffolding.** The compiler's rewrite in Flash begins under `selfhost/`, seeded with `support.flash` — the one module allowed to import Zig's `std` (allocator, containers, string utilities, formatting, file IO, process surface, test hooks); every other selfhost module is pure Flash and imports only `support` and its siblings. Three new build steps wire the migration: `zig build stage1` composes a source directory from the handwritten compiler sources plus each Flash-authored module transpiled by the freshly built `flashc`, and compiles the result as `flashc-stage1` (no modules are swapped yet — the identity hybrid proves the wiring); `zig build test-selfhost` transpiles each selfhost module and runs its Flash `test` blocks, wired into `zig build test`; and `zig build diff-corpus` runs both binaries over every example, probe, and test source and asserts byte-identical behaviour across transpile, `--dump-tokens`, and `fmt` — including diagnostics and exit status on the files the compiler rejects. - **Native compile-time evaluation.** `flashc` now evaluates constant initializers at compile time: integer, boolean, and string folding (arithmetic, comparison, logical, and bitwise operators), references to already-evaluated constants, builtin type names, composite type expressions, and `if`-expressions with a known condition. Definite compile-time errors — a division or remainder by a known zero — are reported at the Flash source line instead of surfacing later from the emitted Zig. Everything beyond the evaluator's reach is left to the downstream compiler exactly as before, and emitted Zig is byte-for-byte unchanged. - **Native generic application checking.** Applying a generic declaration — `Name(args…)` in type position (a parameter, return, binding, or field annotation) or as a value-position call — is now checked by `flashc` itself: argument arity must match the declaration, a `type`-typed parameter rejects a known value argument, and a concretely typed parameter rejects a type argument, each reported at the Flash source line. Parameter kinds resolve through file-scope type aliases; anything unresolvable (a parameter type naming another parameter, an imported generic, an argument only known at runtime) stays silently deferred to the downstream compiler, and emitted Zig is byte-for-byte unchanged. A well-formed application over fully known arguments is also *instantiated*: the same generic applied to the same arguments is the same type (evaluated once), a generic whose body is a single `return` of a type expression folds to the type it denotes — so an alias like `const B = Box(u8)` is a known type to every later check — and a returned container definition is a distinct type per argument list, exactly as it behaves when compiled. Runaway recursive instantiation is caught with a targeted error (a depth limit of 64 and an overall budget) instead of hanging the compiler. - **Composite-type aliases.** A `?`/`*`/`[`/`fn`-led composite type is now an expression, so a type alias needs no wrapper: `const F = *fn(u8) u8`, `const O = ?u8`, `const S = []u8` parse at file scope and inside functions, and a composite type can sit directly in a generic argument (`List([]u8)`). A booked v0.5 arrival under the v1 freeze's additive-only rule; emitted Zig for existing programs is unchanged. The ident-led error union (`E!T`) stays unspellable as an alias value — name the set and compose, as before. - **`defer { … }` / `errdefer { … }` block form.** `defer` and `errdefer` now accept a brace-delimited block body for deferring a sequence of statements; the single-statement form is unchanged. The block opens its own scope and lowers to the identical Zig block form. Another booked v0.5 arrival under the additive-only rule. - **`test "name" { }` blocks.** Flash source can now declare string-named test blocks at file scope, lowering one-to-one to Zig `test` blocks. The new `zig build test-flash` step transpiles each file under `tests/flash/` with the freshly built `flashc` and runs the emitted tests, so Flash code tests itself end to end; the step is also wired into `zig build test`. `test` joins the reserved keywords, and the editor grammars (VS Code, Neovim) highlight it. Another booked v0.5 arrival under the additive-only rule. - **Methods, constants, and imports inside `enum` and `union` bodies.** The associated declarations a `struct` body already carries — `fn` methods, `const` constants, and `use` imports — are now accepted inside `enum` and `union` bodies too, under the same layout rule: variants first, then the declarations. Lowers one-to-one to Zig's native container declarations. Another booked v0.5 arrival under the additive-only rule. - **Loop `else` arms and the `if … else |err|` capture.** A `while` or `for` loop now takes an `else` arm, run when the loop ends without `break` — the search-loop idiom without a flag variable. The `else` arm of an `if` or `while` over an error union can bind the error with `else |err| { … }`, completing the error-capture surface `catch |err|` opened. A `for` else takes no capture (there is no error to bind), and a stray one is rejected with a guiding diagnostic. All forms lower one-to-one to Zig's native loop-`else` and error-capture arms. Another booked v0.5 arrival under the additive-only rule. - **Tuple types and multi-value return.** A parenthesized list of two or more types is a tuple type — `fn pair() (u8, bool)` — spellable wherever a type is: return and parameter positions, aliases (`const Pair = (u8, bool)`), and nested as an element. A `return` heading a statement takes a comma-separated value list (`return 42, true`), Go's multi-return idiom; the tuple value literal stays `.{ … }` and elements read by index (`t[0]`). Lowers to Zig's native tuples (`struct { u8, bool }`, `return .{ 42, true }`). A one-element `(T)` stays plain grouping, and a parenthesized value list gets a diagnostic steering to `.{ … }`. Another booked v0.5 arrival under the additive-only rule. - **Destructuring binds and assignments.** A tuple value now unpacks in one statement: `a, b := pair()` declares both names immutably, `var x, y = pair()` declares them mutable — one keyword rules all names — and `_` skips a position (`tok, _ := next()`). Existing lvalues take a destructuring assignment with plain `=` (`x, arr[0] = pair()`), member and index targets included. All forms lower one-to-one to Zig's native destructures (`const a, const b = pair();`), and the formatter's `:=` canon extends to the new form. Misuses get targeted diagnostics: a compound operator cannot destructure, a `:=` target must be a plain name, and an all-underscore destructure is steered to `_ = expr`. Another booked v0.5 arrival under the additive-only rule. - **`inline for`.** The compile-time-unrolled loop now covers `for` as well as `while`: `inline for x in xs { … }` unrolls the body per element, and every `for` shape rides along — the range iterator (`inline for i in 0..n`), the indexed second capture (`inline for x, i in xs`), the `_` discard, and the loop `else` arm. Lowers one-to-one to Zig's `inline for`. An `inline` followed by anything but a loop gets a targeted diagnostic. The last of the v1 freeze's booked v0.5 loop forms. ### Fixed - A mutable binding of a composite type value (`var F = *fn(u8) u8`) is now rejected at the Flash source line with a guiding diagnostic — a type value is compile-time-only, and the lowered `var` of type `type` was previously left for the downstream compiler to reject against the emitted file. - `flashc fmt` no longer crashes on an `if` whose `else` arm is empty (`if c {} else {}`) — the formatter indexed the arm's first statement for comment-boundary bookkeeping without guarding the empty case. - A parameter referenced only in a returned type expression or a returned container's data shape is no longer flagged unused: `fn Opt(comptime T type) type { return ?T }`, a struct field's type (`return struct { item T }`), a union variant's payload, an enum variant's explicit discriminant, and an array length (`return [n]u8`) all mark the bindings they reference. Previously such generics — valid downstream — were rejected with `unused parameter`. ## [0.4.0] - 2026-06-10 ### Added - **The v1 syntax surface is frozen.** This release ratifies the Flash v1 grammar; from v0.4.0 the surface grows additively only, and only where the freeze document books a construct's arrival. The grammar was regenerated from the implementation to match it exactly, the statement-boundary and capture-pipe disambiguation rules were codified, and the four-class reserved-word inventory, the lexical grammar, and the Zig-exact operator-precedence table were pinned. - **The interactive tutorial is hosted on GitHub Pages.** A `Pages` workflow (`.github/workflows/pages.yml`) deploys `tutorial/public/` on every push to `main`, so the eleven chapters and the code editor are readable in any browser at `ajhahnde.github.io/Flash` with no local setup. The frontend probes for the transpile backend and degrades gracefully on static hosting (chapters and load-into-editor work; live Flash → Zig transpilation points to the local dev server). Every doc page links the tutorial in its nav row, and the README gains a Tutorial section. - **`catch` block recovery.** A `catch` handler may now be a block — `e catch { … }` and `e catch |err| { … }` — for multi-statement error recovery, beside the existing expression handler (`e catch 0`, `e catch return err`); it lowers to the identical Zig. The block is the common idiom for discarding a void operation's error (`writer.flush() catch {}`); a recovery that produces a value still uses the expression handler. - **`flashc fmt`, the comment-preserving formatter.** Flash now has a canonical source format and a tool that enforces it — gofmt / zig fmt for Flash. `flashc fmt ` rewrites a file to one layout: four-space indent, one blank line between top-level units, the brace spacing zig fmt uses, mandatory braces, an untyped immutable binding in the `name := value` short-declaration form, and statement conditions without parentheses (a value `if` keeps them). `flashc fmt --check ` writes nothing and exits non-zero when a file is not already canonical, for CI and pre-commit use. The formatter parses the file first and refuses it untouched on a syntax error — it never destroys code — and it does not run the semantic checks (it formats unchecked source, as gofmt does). Three guarantees back the rewrite, each gated by the test suite: every comment in the input is preserved exactly once; formatting never changes the emitted Zig (`lower(parse(src))` is byte-identical before and after); and the formatter is idempotent. Preserving comments required lexing them as tokens — line comments (`//`, `////`, `//!`, every non-doc `//…` shape) are no longer discarded as whitespace, so `flashc --dump-tokens` now lists them too, though the grammar never sees a comment token and every program still lowers to byte-identical Zig. The shell helper gains a `flash fmt ` verb. - **Native semantic checker with located diagnostics.** Flash's own semantic checks now report against the `.flash` source instead of leaving every error to the emitted Zig: each diagnostic prints `file:line:col: error: …` at the offending column, and the checker collects every problem in one pass rather than failing on the first. Positions are recovered from the syntax tree's source slices, so the tree carries no span bookkeeping of its own. The first check on this footing is member-access root resolution — an `X.field…` whose root `X` is no import, parameter, or binding in scope now reads `unknown name 'X': not an import, parameter, or binding in scope`, pointing at `X`, where before it was a single unlocated line. Assignment to an immutable binding is now rejected too: storing to a `const`/`:=` local, a global `const`, a parameter, or a loop/capture name reads `cannot assign to immutable …` with a note at the declaration, while a `var` binding stays assignable. Only a bare-identifier target is judged — a projection (`s.f`, `a[i]`, `p.*`) turns on aggregate mutability the Tier-0 pass has no types for and stays the downstream checker's job. A local binding, parameter, or capture that is declared and never referenced is now reported as `unused …`; the escapes are the `_` placeholder, a `_ = name` discard, and the new `for _ in …` capture. A binding referenced only in type position — a local type alias, or a `comptime T type` parameter used only in a signature — counts as used, and a bodyless `extern` prototype's parameters are exempt. Finally, a bare expression statement that only produces a value — a stray `a == b`, a lone identifier, or a continuation line a leading `-`/`&` accidentally split off — is reported as `expression value is ignored`, surfacing the statement-split hazard at the `.flash` line; an effectful or control-flow statement (a call, `try`/`catch`, a `switch`, `return`, `unreachable`, …) is exempt. Reusing a name already in scope is rejected — a second binding of one name in a scope is a `redeclaration`, and a binding, parameter, or capture reusing a name visible from an enclosing scope (an outer local, a parameter, a file-scope declaration) reads `'name' shadows …`; Flash forbids shadowing outright, matching Zig and the stricter-than-Zig brand, each diagnostic carrying a note at the prior declaration. These checks run inside struct method bodies as well as free functions. - **`_` as a `for`-loop capture.** A `for` loop can discard its element or index capture with `_`: `for _ in 0..n { … }` repeats `n` times without binding a counter, and `for x, _ in xs { … }` iterates elements while dropping the index. Both lower verbatim to Zig's `|_|` / `|x, _|`. Only `for` captures take `_` — a `catch` or switch-prong capture is instead omitted entirely, matching Zig — and the discard is what keeps a repeat-`n` loop writable now that an unused capture is an error. - **Wrapping subtraction and multiplication operators `-%` / `*%`.** The wrapping arithmetic set is now complete: `-%` (wrapping subtract) joins `+%` on the additive tier and `*%` (wrapping multiply) sits on the multiplicative tier — the same precedence split as `-` versus `*`. Both lower verbatim, since Zig spells them identically, so `a -% b *% c` emits byte-for-byte what `zig fmt` produces with no added parentheses. They complete the kernel's wrapping-arithmetic vocabulary (only `+%` existed before). - **Reserved value and primitive-type keywords.** The literal words `true`, `false`, `null`, `undefined`, and `unreachable`, and the primitive-type words `noreturn`, `anytype`, and `anyopaque`, are now reserved keywords rather than ordinary identifiers. The value words parse to a dedicated value-expression leaf and the type words to a plain named type; all eight are spelled identically in Zig, so each lowers verbatim and the emitted output is byte-for-byte unchanged across the corpus (`zig fmt`- and `zig ast-check`-clean, no migration). Reserving them is what makes `undefined := 5` — binding a reserved word as a name — a parse error instead of a silent shadow of the language constant, and it pins down the reserved-word set the upcoming syntax freeze must enumerate. Every corpus use was already in value or type position, never a name, so nothing changes meaning. - **For-loop range and indexed-capture surface.** A `for` loop can now iterate a half-open range and bind a running index, replacing the counter-`while` ceremony that dominates the corpus. `for i in lo..hi { … }` lowers to Zig's `for (lo..hi) |i|` — a counted loop in one line instead of a three-line `var i = 0; while i < n : (i += 1)`. A second capture names the index: `for x, i in xs { … }` lowers to `for (xs, 0..) |x, i|`, pairing each element with its position. The bare `for x in xs` is unchanged and still lowers byte-for-byte to `for (xs) |x|`. The range uses the existing `..` token in the iterator position only (it is not a general range expression, as in Zig); the index is always the implicit `0..`, so parallel two-iterable iteration is not spelled. Both forms are additive — no existing loop changes meaning — and emit Zig that is byte-identical to `zig fmt` and `zig ast-check`-clean. - **`while` optional-capture.** A `while` can now bind a payload, the iterator idiom: `while it.next() |x| { … }` lowers to Zig's `while (it.next()) |x|`, binding each non-null / error-union payload for the body. It is shaped exactly like the `if` optional-capture and uses the same `| ident | {` lookahead, so a bitwise-or condition (`while flags | 1 != 0`) is unaffected. The form was always part of the capture design but was a parse error until now — `while` never consumed the `|x|`. The emitted Zig is byte-identical to `zig fmt`. - **Complete literal lexis.** Flash now recognises all four integer bases — decimal, `0x` hexadecimal, `0o` octal, and `0b` binary — with `_` digit separators in all forms (`1_000`, `0xFF_AA`, `0b1010_1010`, `0o7_7_7`). A letter or out-of-base digit immediately adjacent to a numeric literal is a lexer error, eliminating a class of silent mis-lexing bugs (previously `0o755` split as two tokens and passed `flashc` silently). Float literals are now first-class: `3.14`, `1.5e-3`, `9.81e+0` produce a `.float` token and an `Expr.float` AST node that lowers verbatim, byte-identical to Zig. The char and string escape set is fully specified: simple escapes `\n \r \t \0 \\ \' \"`, hex byte `\xNN` (two hex digits), and Unicode scalar `\u{NNNNNN}`. `lexChar` now correctly handles multi-byte escape sequences — `'\x1b'` was previously `.invalid` and is now a valid char token. - **Error model — origination, named sets, and explicit unions.** Flash can now *originate* errors, not only consume them. `error` becomes a keyword heading two forms: `error.Name`, an error-value origination (`return error.NotFound`), and `error{ A, B }`, a named error-set definition (`const AllocError = error{ OutOfMemory }`). The type grammar gains the infix error union `E!T`, which names the error set explicitly (`fn open(p cstr) AllocError!i32`), alongside the prefix `!T` whose set the compiler infers. An inferred `!T` is valid only on a function *declaration's* return; a function *type* (`*fn(…) !T`) must name its set, and the parser reports the inferred form there with a Flash diagnostic instead of emitting Zig that cannot compile. Every form lowers verbatim and is byte-identical to its `zig fmt` shape. This completes the error half of the explicit-abstraction substrate — an allocator interface can return `AllocError![]u8`. - **Function-pointer types.** The type grammar gains `fn(P, …) R`, a function *type* whose parameters are bare, unnamed types and whose return is optional (a missing return is `void`). A function *pointer* is this type behind a pointer: `*fn(…) R` lowers to Zig's `*const fn (…) R` — const-pointee by default like any `*T` — and `*mut fn(…) R` to a mutable `*fn (…) R`; `?` and the parameter/return types compose as on any other type. This is the language substrate for the explicit `{ ptr, *VTable }` dispatch pattern that hand-written v-tables and the allocator interface rely on (the type-erasure half, `anyopaque`, already passes through). The emitted Zig is byte-identical and `zig ast-check`-clean. - **Inline assembly.** The expression grammar gains `asm [volatile] (template [: outputs [: inputs [: clobbers]]])`. The template is a string or a `\\` multiline string of instruction text; the output and input operands are `[name] "constraint" (body)` lists, where a body is a return-type output (`-> T`) or an expression operand; the clobbers are a single trailing expression (`.{ .memory = true }`). The colon sections are positional — an empty earlier section keeps its `:`. The template and the constraint strings are an irreducible assembler/LLVM sublanguage that passes through unchanged; Flash adds no spelling of its own. The emitted Zig is byte-identical to its `zig fmt` form and `zig ast-check`-clean. This settles the low-level register, barrier, and system-instruction surface the systems layer needs. - **Sentinel-terminated array types.** The type grammar gains `[N:s]T`, a fixed-length array carrying a trailing sentinel (`[4:0]u8` is four bytes plus a guaranteed `0` at index 4), and its inferred-length form `[_:s]T`, whose length comes from the initializer. The canonical use is the null-terminated argument vector an exec call wants — `[_:null]?[*:0]u8{ … }`. Both compose under `?` / `*` / `[N]` like any element type and lower verbatim with no spaces around the `:`, byte-identical to `zig fmt` and `zig ast-check`-clean. This completes the array side of the sentinel-terminated family alongside the existing pointer (`[*:s]T`) and slice (`[:s]T`) forms. - **Generic type application in type position.** A generic instance can now be named where a type is expected — a parameter, a field, a return: `fn f(items List(u8))`, `Map(Key, Val)`, the dotted `pkg.Ring(64)`. The arguments are parsed exactly as a value-position call's, so a type-name argument is an identifier (`List(u8)`) and a value argument is a literal (`Ring(64)`); a composite-type argument that is not itself an expression (`List([]u8)`) is named through an alias. The form lowers verbatim — the call zig already reads in a type position — byte-identical to `zig fmt` and, with the generic declared, `zig ast-check`-clean. Generic *value*-position application (`const L = List(u8)`) already parsed; this closes the type-position half, so a generic that is kept can be written inline, not only aliased. - **Imports inside struct bodies.** A `use` import is now accepted inside a `struct { … }` body, alongside its methods and associated constants, lowering to a struct-level `const NAME = @import(…)` (indented like any member). It is the same `use` form as at the top level — bare-name, quoted file, and `pub use` re-export all apply — so a sibling module is one spelling at every scope. This retires the in-struct workaround `const sys = #import("syscalls.zig")`, the third import spelling, leaving `use` as the single import story. The change is additive (the `#import` builtin still works as an ordinary call); the two corpus driver-select sites migrated in lockstep, and the emitted Zig is byte-identical to `zig fmt`. - **Top-level `var` — mutable globals.** A `var` (and `pub var`) is now spellable at file scope — `var counter usize = 0` lowers to `var counter: usize = 0;` — alongside the existing top-level `const`. It was previously unspellable (`parseItem` had no `var` arm) and undocumented, an accidental strictness; a systems language needs globals. The form is the item-level binding shape with the keyword flipped, so the optional type and required initializer match `const`; the emitted Zig is byte-identical to `zig fmt`. The C-ABI global forms `export var` / `extern var` remain booked for the v0.6 additive window. ### Changed - **Brand gold retuned to a modern amber ladder.** The single Atom One Dark gold `#E5C07B` washed out on light backgrounds, so the accent is now graded per surface: `#FBBF24` on dark, `#F59E0B` as the brand mid tone (light-mode logo, README badges), `#D97706` where amber must carry small text on white. Both logo assets re-rendered. - **Public docs and the tutorial now share one design system.** Every doc page carries the same centered header — logo, title, one-line tagline, nav row with the current page bold — and the prev/next footer; the README badges are unified flat-square. The tutorial web app replaces its ⚡ emoji with the Orbitron wordmark, trades the purple UI accent for the brand amber, and gets a quieter, more readable chrome: pill buttons, a reading-width content column, GitHub-semantic callout colors. Editor syntax highlighting stays Atom One Dark / One Light verbatim. - **`undefined` is rejected as the value of an immutable binding.** A mutable `var buf: [N]u8 = undefined` — the deliberate uninitialised-storage pattern — is unchanged, but an *immutable* binding set to `undefined` (`const x = undefined`, a typed `const x: T = undefined`, or a `:=` short binding) is now a compile error. An immutable binding can never be given a real value afterwards, so an `undefined` initialiser is always a mistake; the diagnostic points at `var`. This is stricter than Zig, which accepts the form and lets a later read be undefined behaviour. No corpus binding used it — every corpus `= undefined` is a `var` or a struct-field default, both of which stay valid. - **The `argv` / `cstr` builtin type aliases now yield to a user declaration.** In type position the compiler still expands the two builtin spelling aliases — `cstr` to `[*:0]const u8` and `argv` to `[*]const ?[*:0]const u8` — that the coreutils need but the surface gives no syntax for. Previously the rewrite was unconditional and silently overrode a program's own top-level `const cstr = …` (or `argv`) binding; now such a binding suppresses the builtin alias for that name throughout the file, so the user's — or a future standard library's — alias wins instead of being quietly replaced. No corpus program declares either name at file scope, so every emitted byte is unchanged. - **Compiler intrinsics now use a `#` sigil instead of `@`.** Every intrinsic is respelled `#name` — `#intCast`, `#ptrCast`, `#bitCast`, `#as`, `#truncate`, `#alignCast`, `#intFromPtr`, `#ptrFromInt`, `#import`, `#embedFile`, `#memcpy`, `#export`, … — removing the last visually Zig-specific token from the surface. Each intrinsic keeps its exact semantics; only the sigil changes. The parser stores the bare intrinsic name and lowering re-sigils `#name` to Zig's `@name`, so the emitted Zig is byte-identical and stays diffable (`zig ast-check`-clean). The VS Code and Neovim grammars and the tutorial are updated in lockstep. - **Many-item pointers are now const-pointee by default.** `[*]T` and the sentinel form `[*:s]T` name a const pointee, matching slices (`[]T`) and single pointers (`*T`); the writable forms are `[*]mut T` / `[*:s]mut T`. The word `const` is no longer spellable in a many-item-pointer type — `[*]const T` was the old read-only marker and is now a parse error whose message points at the new spelling. This removes the last place the type grammar carried two mutability vocabularies: previously `[*]T` was *mutable* by default, the opposite of every other pointer family. Existing code migrates mechanically — a genuinely-writable `[*]T` becomes `[*]mut T`, an explicit `[*]const T` becomes plain `[*]T` — and the emitted Zig is unchanged. The volatile pointer forms are brought onto the same const-default footing in the matrix change below. - **Volatile pointers are now const-pointee by default, with the single-item forms added.** Volatile becomes an orthogonal qualifier on the const-default brand rather than a mutability vocabulary of its own. The many-item `[*]volatile T` — shipped mutable in 0.3.0 — now names a **const+volatile** pointee (lowering to Zig `[*]const volatile T`), and its writable form is the new `[*]mut volatile T` (Zig `[*]volatile T`). The single-item forms join the grammar: `*volatile T` (const+volatile, `*const volatile T`) and `*mut volatile T` (writable, `*volatile T`) — the missing MMIO single-register shape (a read-only status register is `*volatile T`, a writable control register `*mut volatile T`). The `volatile` qualifier always follows the optional `mut`, mirroring where Zig places `const` before `volatile`; `const` itself is never spellable, as on every other pointer family. This is the breaking half: a `[*]volatile T` written in 0.3.0 to mean a writable region must add `mut` (`[*]mut volatile T`) — a mechanical respelling whose emitted Zig is byte-identical for the writable case. With this, every pointer family — slice, single, many, sentinel, and volatile — is const-by-default with one uniform `mut` opt-in. - **Quoted file imports name the module stem, without a file extension.** A sibling-file import is now written `use "syscalls" as sys` rather than `use "syscalls.zig" as sys`; lowering supplies the backend artifact suffix, so the emitted Zig is unchanged (`const sys = @import("syscalls.zig");`). The extension was a backend implementation detail baked into the surface — a name that would lie once the source becomes the artifact (`"io.zig"` naming an `io.flash`). A quoted import carrying an extension is now a parse error whose message points at the extensionless form. Bare-name module imports (`use flibc`) are unaffected. The example corpus migrated in lockstep (13 sites). - **A value `if` now requires parentheses around its condition.** In value position the condition must be parenthesised — `const x = if (c) a else b` — exactly as Zig spells every `if`; the paren-free `if c a else b` is now a parse error with a migration hint. A *statement* `if` is unchanged and stays paren-free (`if c { … }`), because its `{` already delimits the condition. The paren-free value form was genuinely ambiguous to a reader (`if pos < BUF_LEN pos else …`) and forced two different workarounds in the corpus for the same enum-literal pitfall (a `.tag` then-arm gluing onto the condition as a member access). The lowered Zig is unchanged — the condition was always emitted parenthesised — so this only tightens the surface. Five corpus value-`if` sites gained their parens in lockstep. - **The `->` return arrow is dropped from function signatures.** A return type now follows the parameter list directly — `fn add(a i32, b i32) i32`, the function type `fn(i32) i32`, the pointer `*fn(i32) i32` — exactly as Go and Zig write it; `fn f() -> R` no longer parses. A missing return type is still `void` (`fn log(s []u8) { … }`), and a bodyless `extern fn flush()` is correctly void. Neither Go nor Zig spells the arrow, and it was the largest remaining Flash-chosen sigil; dropping it settles the long-standing contradiction between the surface direction (which always specified no arrow) and the earlier implementation that shipped one. The emitted Zig is unchanged. The `->` token itself is retained for the inline-assembly output operand (`(-> T)`), which mirrors Zig's assembly sublanguage. 62 corpus signatures migrated in lockstep. ### Removed - **The `while` continue-expression `while c : (e)` is gone — one loop spelling.** With range-for landed, the Zig-style continue clause is removed: a bounded counter is a range-`for` (`for i in 0..n`), and any other stepping loop moves its step into the body (`while c { … ; step }`) — exactly as Go, which has no continue-expression. This retires two spellings of one counted loop. A stray `:` after a `while` condition now reports a migration hint pointing at the range-for or body-step form rather than a generic parse error. The example corpus migrated in lockstep: 10 bounded counters became range-`for`, the other 17 (sentinel scans, compound conditions, countdowns, non-unit steps) moved the step into the body, each migration behaviour-preserving. ### Fixed - **A `comptime const` binding now emits valid Zig.** A compile-time immutable local — `comptime const N = e` — previously lowered to the literal `comptime const N = e`, which Zig rejects as redundant ("'comptime const' is redundant; instead wrap the initialization expression with 'comptime'"). It now lowers to `const N = comptime e`, moving the compile-time evaluation onto the initializer: the binding stays a `const`, and the value is still forced to be known at compile time, so a runtime-capable initializer (`comptime const M = size(8)` → `const M = comptime size(8)`) is evaluated at compile time rather than silently demoted to a runtime constant. A `comptime var` keeps its prefix unchanged — it is valid Zig — and a `comptime const` over a string literal, already compile-time-known, emits a plain `const`. No example used the form, so the transpiled corpus is byte-for-byte unchanged; the new behaviour is covered by a lowering test and a `zig ast-check` probe. ## [0.3.0] - 2026-06-08 ### Added - **Struct and enum type definitions.** `const Name = struct { field T, … }` defines a struct (the type follows the field name, no `:`), and `const Name = enum { a, b }` — or `enum(T) { … }` with an explicit backing integer type — defines an enum. Both lower to their idiomatic Zig (`struct { field: T, … }` / `enum { a, b }`). - **Tagged-union type definitions.** `const Name = union(enum) { a, b T, … }` defines a tagged union: each variant is name-first like a struct field, with an optional payload type (a bare name is a void variant, `b T` carries a payload). The selector inside `union(…)` may be the `enum` keyword (a compiler-inferred tag) or a named tag enum (`union(Tag)`); a bare `union` is untagged. It lowers to the idiomatic Zig `union(enum) { a, b: T, … }`. - **`pub` visibility on declarations.** A top-level `const` or `fn` — and an associated constant or method inside a `struct` — may be marked `pub` to export it to importing modules (`pub const MAX = 16`, `pub fn open() …`). `pub` precedes `export` when a function carries both (`pub export fn`). It lowers to the identically spelled Zig `pub` prefix. - **`inline` functions.** A function — top-level or a struct method — may be marked `inline` to request always-inlining (`inline fn min(a u32, b u32) -> u32`). `inline` occupies the same modifier slot as `export`, so the two never co-occur, and it follows `pub` when both are present (`pub inline fn`). It lowers to the identically spelled Zig `inline` prefix. - **Explicit enum discriminants.** An enum variant may fix its value with `= discriminant` (`enum(u8) { ok = 0, perm = 1, again, io = 5 }`); explicit and implicit variants mix freely. The discriminant is an ordinary expression — a decimal or `0x` hex literal, a negative value — lowered verbatim into the idiomatic Zig `enum(T) { name = value, … }`. - **Bitwise and shift operators.** Binary `&`, `|`, `^`, `<<`, and `>>` join the expression grammar at their Zig precedence — shift binds tighter than the bitwise operators, which in turn bind tighter than the comparisons — so flag and register math (`flags & mask`, `lo | hi`, `1 << n`) lowers verbatim to idiomatic Zig. This also unblocks shift-valued enum discriminants (`exec = 1 << 2`). Prefix `&` (address-of) is unchanged; it is distinguished from infix `&` by position. - **Bitwise and shift compound assignment.** The in-place forms `&=`, `|=`, `^=`, `<<=`, and `>>=` join the existing `+= -= *= /= %=`, so a bitflag or register update (`flags |= mask`, `bits <<= 1`) is written directly instead of expanded to `flags = flags | mask`. Each lowers verbatim to the identically spelled Zig compound assignment. - **Struct methods and associated constants.** A struct body may carry `fn` methods and `const` declarations after its fields. A method is an ordinary function — its receiver, when it takes one, is a plain first parameter, so there is no implicit `self`. Fields lead and declarations follow; the lowering reproduces zig fmt's container layout (the field block, a blank line, then the declarations one blank line apart). - **Named struct-init and inferred enum literals.** `.{ .x = 1, .y = 2 }` constructs a struct by field name (alongside the existing positional `.{ a, b }` form), and `.red` is an inferred enum literal. Brace spacing follows zig fmt — `.{x}` for a single positional element, `.{ … }` otherwise. - **Multiline / raw string literals.** Zig-style `\\…` lines, with no escape processing, for usage text, box art, and templates. A run of consecutive lines folds into one string; in constant, binding, and discard value position the lowering reproduces the `zig fmt` block layout byte for byte. - **Doc comments.** A `///` documentation comment is preserved through the transpile: a run of `///` lines attaches to the declaration it leads — a top-level `const` or `fn`, a struct field or member, or an enum/union variant — and is re-emitted verbatim before that declaration, byte-for-byte as zig fmt keeps it. Ordinary `//` comments stay trivia; the top-level `//!` module-doc form is not yet preserved. - **Single-item pointer types.** `*T` is a single-item pointer, with a const pointee by default — mirroring the slice convention where `[]T` is a const slice; `*mut T` opts the pointee into mutability. The prefix composes over any element type, including a pointer-to-array (`*[N]T`). They lower to the idiomatic Zig `*const T` / `*T`. - **Sentinel-terminated pointer types.** `[*:s]T` is a many-item pointer terminated by a sentinel value `s` (most often `[*:0]u8`, a nul-terminated string). Unlike the `cstr` / `argv` spelling aliases, it is a real composite element: the prefix nests under `*`, `[N]`, and `?` (e.g. `*[4]?[*:0]u8`). It lowers to the identically spelled Zig `[*:s]T`. - **Pointer dereference.** `p.*` dereferences a single-item pointer — reading the pointee in value position and storing through it as an assignment target (`p.* = v`). It composes as a postfix operator: `pp.*.*` (double dereference), `n.*.field` (dereference then member), and `arr.*[i]` (dereference then index). It lowers to the identically spelled Zig `p.*`. - **Optional unwrap.** `opt.?` unwraps an optional, asserting it is non-null and yielding the payload — the checked counterpart to `orelse` and the optional-capture `if`. Like `.*`, it is a postfix operator told apart from a member access by the token after the dot, so it composes in a chain: `argv[1].?` (index then unwrap), `opt.?.field` (unwrap then member). It lowers to the identically spelled Zig `opt.?`. - **Sentinel-terminated slices.** A slice may assert its end with a sentinel value: `a[lo..hi :s]` (and the open-ended `a[lo.. :s]`) yields a slice known to terminate at `s`, so `buf[lo..hi :0].ptr` produces a nul-terminated pointer out of a buffer. The `:s` is independent of the high bound. It lowers to the identically spelled Zig `a[lo..hi :s]` (a space before the `:`, as zig fmt lays it out). - **`while` continue-expression.** A `while` loop may carry a continue clause, `while c : (e) { … }`, whose statement `e` (an assignment such as `i += 1`, or a bare expression) runs after every iteration — including after a `continue` — so a counter-driven loop need not duplicate the step before each `continue`. It lowers to the identically spelled Zig `while (c) : (e) { … }`. - **Struct field default values.** A struct field may carry a default value, `name T = expr`, applied when the container is constructed with that field omitted (`Slot{}`). The default is any expression — a literal, `undefined`, an empty `.{}` — and lowers after the type as the idiomatic Zig `name: T = expr,`. - **Volatile many-item pointers.** `[*]volatile T` is a many-item pointer whose pointee is volatile — accesses through it are never reordered, widened into a wider store, or elided. The `volatile` qualifier sits between the `]` and the element type. It lowers to the identically spelled Zig `[*]volatile T`. - **Const-pointee many-item pointers.** `[*]const T` (and the sentinel form `[*:s]const T`) is a many-item pointer whose pointee is read-only. Unlike a slice — `[]T` is const by default — a many-item pointer is mutable by default, so the read-only form takes an explicit `const`, placed where Zig places it: after the `]`, before the element type. It lowers to the identically spelled Zig `[*]const T` / `[*:s]const T`. - **Sentinel-terminated slice types.** `[:s]T` is a slice whose end is marked by a sentinel value `s` — `[:0]u8` is the nul-terminated byte slice a path resolver hands back for a C string. Like a plain `[]T` it is const by default, with `[:s]mut T` opting into a mutable element; the type is distinct from the sentinel slice *operation* `a[lo..hi :s]` that produces such a slice out of a buffer. It lowers to the identically spelled Zig `[:s]const T` / `[:s]T`. - **If-expressions.** `if cond a else b` is an `if` used for its value — both arms are expressions and `else` is required, since an if-expression always yields a value. The condition is paren-less, as in the statement form. It lowers to the idiomatic Zig `if (cond) a else b`. - **Type-valued if-expression arms.** An if-expression's arms may be `struct { … }` type definitions, so a comptime gate can select between two implementations — `const driver = if has_driver struct { … } else struct { … }` picks the real target driver or a host stub, the idiom that keeps off-target inline-asm out of a host build. The selected struct renders at the enclosing declaration's indent. It lowers to the identically spelled Zig `if (cond) struct { … } else struct { … }`. - **Parenthesised if-expression conditions.** A value `if`-expression may wrap its condition in parentheses — `if (best > typed) .progressed else .stuck` — to mark where the condition ends and the then-arm begins. This is needed when the then-arm is an enum literal (or another `.`-leading form): paren-less, the leading `.` would otherwise read as a member access on the condition's tail. A binary operator after the `)` keeps the parentheses as an ordinary sub-expression group (`if (a || b) && c …`). It lowers to Zig's own `if (cond) …` with a single pair of parentheses. - **Labeled block expressions.** `label: { … }` is a block used for its value — whatever a `break :label value` inside it yields — so a multi-statement computation can stand where a value is expected (a `switch` prong, a binding). `break` correspondingly gains a target label and an optional value (`break :blk`, `break :blk v`) alongside the bare loop `break`. They lower to the identically spelled Zig. - **Typed struct/union literals.** `Type{ .x = 1 }` (and the empty `Type{}`) is an initializer whose type is named rather than inferred from context — the counterpart to the anonymous `.{ … }`. To keep the paren-less control-flow headers unambiguous, a `{` immediately after a header subject opens the body, not a literal (`while p.len { … }` is a loop); a literal in that position must sit inside parentheses or a call, exactly as Go resolves the same ambiguity. It lowers to the identically spelled Zig `Type{ … }`. - **`switch` expressions.** `switch subject { pat => body, … }` matches a scrutinee against prongs: a single value, a comma-separated value list (`'\r', '\n' => …`), an inclusive range (`0x20...0x7e => …`), or the default `else`. A prong body is any expression — an inline value, an `if`-expression, or a `label: { … }` block for a multi-statement arm. The subject is paren-less, as in the other control-flow headers. It lowers to the idiomatic Zig `switch (subject) { … }`. - **Switch-prong payload captures and unlabelled block arms.** A switch prong may bind the active union variant's payload with a `=> |x|` capture (`.single => |n| runSingle(&argv, n)`) — the checked counterpart to the optional-capture `if` and `catch |e|`, in scope for that prong's body only — and a `=> |e| switch e { … }` re-matches the captured payload. A prong body may now also be an unlabelled block: a void `.empty => {}` arm or a multi-statement arm, joining the existing inline-value, `if`-expression, and `label: { … }` forms. Both lower to the identically spelled Zig (`.single => |n| …`, `=> {}`). - **`align(N)` binding qualifier.** A `var` / `const` binding may carry an `align(expr)` qualifier between its type and `=` (`const comp T align(16) = …`), forcing the binding's alignment. It is needed where a value is materialised on the stack and must dodge a strict-align vector store — the shell REPL's >16-byte `Completion` takes `align(16)` so an LLVM 16-byte NEON store into an 8-aligned slot cannot fault. It lowers to the identically spelled Zig `align(N)`. - **Bodyless `extern fn`, signature `callconv`, and `comptime` blocks.** A function may be declared without a body as an `extern fn` prototype (`extern fn main(argc usize, argv argv) callconv(.c) -> noreturn`), naming a C-ABI symbol resolved elsewhere at link time; it lowers to the Zig prototype closed with `;`. Any function may state an explicit calling convention with `callconv(.c)` between the parameter list and the return arrow (an `export fn` still gains the implicit C-ABI marker without one). And a top-level `comptime { … }` block holds statements evaluated at compile time — today an `@export(&f, .{ .name = …, .linkage = .strong })` that forces a symbol's emission. Each lowers to the identically spelled Zig. - **Sibling-file imports.** `use "syscalls.zig" as sys` imports a sibling source file — the target is a quoted path, kept verbatim with its extension — and lowers to `const sys = @import("syscalls.zig")`. A bare-name `use flibc` remains a module import (`@import("flibc")`); the quotes are what distinguish a file from a module, mirroring Zig's own `@import` and the existing `link "M"` string form. - **Re-exported imports (`pub use`).** A `use` import may be marked `pub` to re-export it from the importing module — `pub use "io.zig" as io` lowers to `pub const io = @import("io.zig")` — so a hub module surfaces its sub-modules' surface one level deep (a consumer reaches `flibc.printf` / `flibc.Dirent` without naming each leaf). A bare `use` stays a private import, and `pub use` packs in the same import run as the private form. - **Unary bitwise-NOT `~`.** The prefix `~` complements every bit of its operand, joining `!`, `-`, and prefix `&` at the prefix-operator tier (tighter than every binary operator), so an alignment mask (`~(ALIGN - 1)`) or a register-bit clear is written directly. It lowers to the identically spelled Zig `~`. - **Concatenation `++` and wrapping addition `+%`.** Two binary operators join the additive precedence tier: `++` concatenates arrays and slices (`prefix ++ &[_]u8{c}`), and `+%` is two's-complement wrapping addition (`val +% 1`, used to recover a magnitude past `i64`'s minimum without tripping the overflow check). Each lowers to the identically spelled Zig. - **`comptime` parameters and bindings.** A parameter may be marked `comptime` (`comptime fmt []u8`) to force a compile-time-known argument, and a local binding may be marked `comptime` (`comptime var i usize = 0`) to evaluate at compile time — the two together let a function walk one of its arguments at codegen time. The qualifier precedes the parameter name / the `var`/`const`, as in Zig, and lowers to the identically spelled `comptime ` prefix. - **`inline while` loops.** A `while` loop may be marked `inline` to unroll it at compile time (`inline while i < fmt.len { … }`), which a `comptime` walk over a fixed-length sequence needs so each iteration's body specializes. It lowers to the identically spelled Zig `inline while (…) { … }`. - **Inferred-length array types and literals.** `[_]T` is an array whose length is taken from its initializer, and `[_]T{ … }` is the matching array literal (`&[_]u8{c}` addresses a one-element array). The `_` stands where a length expression would sit. Both lower to the identically spelled Zig `[_]T` / `[_]T{ … }`. - `examples/tokenize.flash` — the fsh command tokenizer, the first FlashOS module ported from its hand-written Zig. It exercises the tagged-union surface end to end: a `union(enum)` result with mixed void and payload variants, union literals including a nested `.{ .piped = .{ … } }`, and bare enum-literal returns (`return .empty`), alongside the composite `*mut [MAX_ARGS]?[*:0]u8` signature, a sentinel slice, and a continue-expression with an empty body. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's canonical layout (mandatory braces, expanded containers). - `examples/readline.flash` — the flibc raw line editor's pure core, the second FlashOS module ported from hand-written Zig. It exercises the expression and statement grammar end to end: a `switch` over the input byte (value lists, an inclusive range, an inline `if`-expression arm, and `label: { … }` block arms whose value is a `break :blk …`), a typed union literal (`Action{ .echo = byte }`), `[*]volatile u8` for the alignment-safe byte copy, struct field default values, struct methods over `*State` / `*History`, and optional (`?[]u8`) returns. Its core lowers to Zig whose token stream matches the reference, modulo Flash's canonical layout (mandatory braces; ordinary `//` comments are not re-emitted). - `examples/mem.flash` — flibc's freestanding C-ABI `mem*` providers (`memset` / `memcpy` / `strlen`), the first `flibc` library leaf ported from hand-written Zig after the `tokenize` and `readline` cores. Three `export fn`s the linker resolves by name, exercising const-pointee many-item pointers (`[*]const u8` / `[*]const u64` copy sources, a `[*:0]const u8` nul scan), an `&&` alignment guard lowering to `and`, and the `@ptrCast` / `@alignCast` / `@intFromPtr` builtins. Its core lowers to Zig whose token stream matches the reference, modulo Flash's canonical layout (each `export fn` gains the explicit `callconv(.c)` C-ABI marker; ordinary `//` comments are not re-emitted). - `examples/start.flash` — flibc's `_start` argc/argv shim, the second `flibc` leaf ported from hand-written Zig. It is the default ELF entry point for programs that take command-line arguments: it forwards `main(argc, argv)` and forces its own `_start` emission. The first port to need a bodyless `extern fn` prototype, an explicit signature `callconv(.c)`, and a top-level `comptime { … }` block; its core lowers to Zig byte-for-byte identical to the reference, modulo the dropped `//` module header. - `examples/process.flash` — flibc's process-glue layer (`fork` / `wait` / `exit` / `execve` / `chdir`), the third `flibc` leaf ported from hand-written Zig. Five thin C-ABI wrappers over the kernel syscall surface, and the first port to use a sibling-file import (`use "syscalls.zig" as sys`). Its core lowers to Zig byte-for-byte identical to the reference, modulo the dropped `//` module header. - `examples/heap.flash` — flibc's bump allocator over the kernel's `brk`/`sbrk` syscalls, the fourth `flibc` leaf ported from hand-written Zig. A state-free `malloc` that rounds each request up to an 8-byte boundary and a no-op `free`; the first port to need the unary bitwise-NOT `~` (the alignment mask `~(ALIGN - 1)`), alongside an optional many-item-pointer return (`?[*]u8`) and an ignored `_` parameter. Its core lowers to Zig byte-for-byte identical to the reference, modulo the dropped `//` module header and Flash's mandatory braces on the two single-statement `if`s. - `examples/flibc.flash` — flibc's re-export hub, the fifth `flibc` leaf ported from hand-written Zig. Thirty-five declarations that pull the sub-modules one level deep and re-export the userland-facing surface; the port that motivated `pub use`, interleaving re-exported imports (`pub use "io.zig" as io`) with bare-module imports (`use syscall_defs as defs`) and value re-exports (`pub const printf = io.printf`). Its declaration stream is token-identical to the reference, modulo the dropped `//` comments and Flash's uniform one-blank-per-declaration layout (the reference's hand-authored grouping blank lines are not preserved). - `examples/execvp.flash` — flibc's bare-name program resolver, the sixth `flibc` leaf ported from hand-written Zig and the first carrying a host/target driver select. A pure `resolve` maps a bare name to `/bin/` (a slashed name passes through) and returns a sentinel-terminated slice `?[:0]u8` into the caller's buffer; a comptime `if`-expression then picks the real aarch64-freestanding `execvp` driver or a host stub. The first port to need the sentinel-terminated slice *type* and type-valued if-expression arms, alongside the `cstr` / `argv` spelling aliases and an in-struct `@import`. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's mandatory braces on the three single-statement `if`s (and the dropped `//` comments). - `examples/io.flash` — flibc's console I/O layer (`puts` and a comptime-format `printf`), the seventh `flibc` leaf ported from hand-written Zig. `printf` walks its format string at compile time — a `comptime fmt` parameter with `args anytype`, `comptime var` counters, and an `inline while` — dispatching each `%`-spec to a `buf_put_*` helper, with the wrapping add `+%` for the signed-magnitude path and a `@compileError("…" ++ &[_]u8{spec})` for an unsupported spec. Its core lowers to Zig whose token stream is identical to the reference (the dropped `//` comments aside) — the `switch` used as a bare statement carries no trailing `;`, matching Zig's block-form statement rule. - `examples/keys.flash` — flibc's console key decoder, the eighth `flibc` leaf ported from hand-written Zig and the first that is a *pure* port: it adds no new grammar. The decoder is a three-state VT100 machine that turns raw console bytes (including the multi-byte ESC-`[` arrow sequences) into semantic `Key` events, reusing surface already landed — value, multi-pattern (`'\r', '\n' =>`) and inclusive-range (`0x20...0x7e =>`) switch prongs, a labeled-block prong (`blk: { … break :blk … }`), a defaulted struct field over a nested `const` enum, `&&` for the comptime gate, and the host/target driver-select `if has_driver struct {…} else struct {…}`. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's mandatory braces on the three single-statement `if`s, the one-per-line enum body, and the dropped `//` comments. - `examples/completion.flash` — flibc's tab-completion core, the ninth `flibc` leaf ported from hand-written Zig. The pure discovery half of the shell's TAB handling: `parse` splits a line into a command-or-path completion context, `hasPrefix` / `commonPrefixLen` are the candidate string folds, and `classify` decides whether a TAB progressed, stuck, or did nothing. It reuses the optional-capture `if (slash) |s| { … }`, `?usize` optionals, `while … : (i += 1)` continue-clauses, and slice expressions — and it surfaced the one new grammar item this cluster needed: a parenthesised value-`if` condition so `classify`'s `if (best_len > prefix_len) .progressed else .stuck` keeps its enum-literal arm. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's mandatory braces on the single-statement `if`s and the dropped `//` comments. - `examples/pager.flash` — flibc's pager core, the tenth `flibc` leaf ported from hand-written Zig and the third *pure* port: it adds no new grammar. The `Pager` indexes the line starts of a slurped text buffer once, then answers a render loop's two questions — which lines are on screen, and where a scroll clamps to — keeping `top` inside `[0, maxTop]`. It is the first struct to mix a value receiver (`self Pager`, the read-only `line` / `maxTop` queries) with a pointer receiver (`self *mut Pager`, the scroll mutators), and the first with a void-returning method, and it leans on the const-default slice convention for its fields — an immutable `text []u8` lowers to `[]const u8`, a mutable `lines []mut u32` to a bare `[]u32` — over reused `@intCast`, value `if`-expressions, and slice expressions. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's mandatory braces on the single-statement `if`s and the dropped `//` comments. - `examples/fsh.flash` — the FlashOS shell's command-execution and built-in layer, ported from hand-written Zig and the leaf that surfaces the `.?` optional-unwrap. Given a tokenized `argv` it runs the command: `runSingle` / `runPiped` fork and `execvp` one program or wire a single `|` pipe stage (dup2 the pipe ends, reap both children), `runBuiltin` dispatches the in-process commands (`cd` / `pwd` / `free` / `whoami` / `reboot` / `exit` / `help`), and `listBin` / `whoami` walk `/bin` and resolve a uid against `/etc/passwd`. The `.?` unwrap drives two spots — `cd` takes its argument or falls back to `"/"` (`if (argc >= 2) argv[1].? else "/"`), and a pipe stage asserts its command name before exec (`left[0].?`) — over the reused `*mut [N]?[*:0]u8` argv vector, sentinel pointers, `orelse`, and the `++` folds of its help text. The line dispatcher (`dispatch` / `runFshrc`, with the `trim` / `isSpace` helpers) and the entry + REPL driver (`main` / `repl`) port too — surfacing the switch-prong payload captures, the void prong body, and the `align(N)` qualifier the REPL's `Completion` needs — so **fsh is now fully ported**: the whole shell, from the `_start` hand-off through the interactive prompt, is Flash. Its core lowers to Zig whose token stream is identical to the reference, modulo Flash's mandatory braces on the single-statement `if`s and the dropped `//` comments. ### Fixed - **Bare `return` stops at a list / prong close.** A value-less `return` used as a switch-prong body (`.eof => return,`) or to the right of `orelse` inside a call or index (`f(a orelse return)`) no longer reads the following `,` / `)` / `]` as a return value — its stop set now matches `break`'s, so the comma after the prong is left for the prong list instead of tripping a parse error. - **Block-form expression statements drop the trailing `;`.** A `switch` (or a labeled block) used as a bare statement, with its value discarded, now lowers without a trailing semicolon — Zig treats it as a block-form statement and rejects the stray `;`. An expression statement that is not block-form still closes with `;`, and a `switch` in value position (behind `return` or an assignment) is unaffected. - **Empty blocks collapse to `{}`.** An empty block — a function body with no statements, or an empty `if` / `else` / `while` / `for` body — now lowers to a one-line `{}`, matching `zig fmt`, instead of a two-line `{\n}`. A continue-expression is still emitted before the braces (`while (c) : (e) {}`). Every block-emitting site shares one collapse rule, so the output is byte-exact for empty-bodied control flow. - **Slice `..` spacing follows `zig fmt`.** A slice expression now spaces its `..` exactly as `zig fmt` does: tight when both bounds are simple (`buf[lo..hi]`, `buf[lo..][0..n]`), spaced when either bound is a binary operation (`buf[lo .. hi + 1]`), and spaced only before `..` when the upper bound is omitted (`buf[lo + 1 ..]`). The sentinel form follows the same rule (`buf[lo .. hi + 1 :0]`). Previously a compound bound was emitted tight, which `zig fmt` would rewrite. - **Top-level declarations resolve as member-access roots.** An `X.field` expression whose root `X` names a top-level `const` or `fn` — such as a tag on a file-scope `union(enum)` (`Action.eof`) — now resolves during name checking instead of being rejected as an unknown root. Previously only imported modules, parameters, and bindings were accepted, so a free function referencing a sibling type tripped a false error. ## [0.2.0] - 2026-06-07 ### Added - **Expression grammar.** Binary operators with precedence (`+ - * / %`, comparisons, `&&` / `||`, `orelse`), prefix unary (`!`, `-`, `&`), indexing and slicing (`xs[i]`, `xs[lo..hi]`), Zig builtin calls (`@intCast(…)`), character and hexadecimal literals, and anonymous struct/tuple literals (`.{ … }`). `&&` / `||` lower to Zig's `and` / `or`; explicit parentheses are preserved so evaluation order is never silently changed. - `examples/meminfo.flash` — the `/bin/meminfo` coreutil, lowering byte-for-byte to its reference Zig. - **Control flow and statements.** `if` / `else` (and `else if`) and `while`, both with parenthesis-free conditions and mandatory braces; `for x in xs` (lowering to Zig's `for (xs) |x|`); `break`, `continue`, and `return` (the first two usable on the right of `orelse`); assignment, including the compound forms `+= -= *= /= %=`. Name resolution is block-scoped. - **More declarations and types.** Top-level named constants (`const NAME T = …`), fixed-size array types (`[N]T`), and the `cstr` alias for a nul-terminated C string (`[*:0]const u8`). - Coreutil ports `examples/{dmesg,echo,cat,ls}.flash`, exercising the new control flow. Because Flash makes braces mandatory, these lower to human-diffable Zig that is semantically equivalent to the reference tools rather than byte-for-byte copies; the straight-line coreutils stay byte-identical. - **Error model — Zig error unions.** Optional (`?T`) and error-union (`!T`) types; `try` to propagate an error union and `catch` (with an optional `|err|` capture) to recover from one; `defer` and `errdefer` for scope-exit cleanup; and the optional-capture `if opt |x| { … }`, which runs its body only when the optional is present (lowering to Zig's `if (opt) |x| { … }`). Errors are values and every failure edge is written in the source — no hidden control flow. - `examples/readfile.flash` and `examples/sysinfo.flash` — coreutil ports built on the error model and the optional-capture `if`, each lowering to human-diffable Zig. ### Changed - **Language surface redesigned (Go-flavored, ligature-friendly).** Function signatures place the type after the name (`fn f(a u8, b []u8) -> Ret`); bindings use `:=` (immutable, inferred type), `var name T = …` (mutable), and `const NAME = …` (named const). The `let` keyword and `:`-typed parameters are removed; `->` is kept for the return type. Statements remain semicolon-free. The emitted Zig is unchanged — `hello.flash` and `clear.flash` still lower to their reference Zig byte for byte. ## [0.1.0] - 2026-06-07 ### Added - Project scaffold: `build.zig` (Zig 0.16 hard pin, the `flashc` executable, `run` and `test` steps, version single-sourced from `build.zig.zon`), the package manifest, `.zigversion`, license, and this changelog. - `VISION.md` — the project's north-star vision: the Tier-0 strategy and the path to a self-hosted compiler and a self-hosted FlashOS. - Lexer (`src/lexer.zig`) for the Flash v0.1 grammar subset: keywords, identifiers, integer and string literals, the `->` arrow, punctuation, `// line comments`, and the lone `_` token. Zero-allocation and span-based, covered by host unit tests. - Token taxonomy (`src/token.zig`). - Parser (`src/parser.zig`) — recursive descent over the full v0.1 grammar, building the AST (`src/ast.zig`). Zero-copy (nodes hold spans into the source) and reports a single-line diagnostic on the first malformed token. Covered by host unit tests. - Semantic checks (`src/sema.zig`) — resolves the root of every member-access chain against the imported modules, parameters, and bindings in scope; an unresolved root is rejected. - Lowering (`src/lower.zig`) — the Tier-0 backend: emits human-diffable Zig source, reproducing the reference lowering for both examples byte for byte. - The `flashc` CLI driver: `--version`, `--help`, `--dump-tokens`, and the full Flash → Zig transpile, with lowered source on stdout and diagnostics on stderr. - Example programs `examples/hello.flash` (the spike target) and `examples/clear.flash` (the cross-import proof). - Integration test suite (`tests/lex_examples.zig`) that lexes the example sources, wired into `zig build test` beside the unit tests. - Logo and brand: an Orbitron wordmark (`assets/flash_logo_light.png` / `assets/flash_logo_dark.png`) — a neutral "F" with an accented gold "lash" (Atom One Dark `#E5C07B`). - `SETUP.md` and `VERSIONING.md`; a GitHub Actions workflow running `zig build test` on push; `flash.env.zsh` shell helpers; a `tools/` placeholder for future host tools. --- [← Prev: Versioning](VERSIONING.md) · [Next: License →](LICENSE.md)