ajhahn.de
← Flash
Markdown 1585 lines
<div align="center">

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="assets/flash_logo_dark.png">
  <img src="assets/flash_logo_light.png" alt="Flash" width="240">
</picture>

<h1>Changelog</h1>

<p><i>Every notable change, release by release.</i></p>

<p>
  <a href="README.md"><b>README</b></a> ·
  <a href="VISION.md"><b>Vision</b></a> ·
  <a href="https://ajhahnde.github.io/Flash/"><b>Tutorial</b></a> ·
  <a href="REFERENCE.md"><b>Reference</b></a> ·
  <a href="COOKBOOK.md"><b>Cookbook</b></a> ·
  <a href="SETUP.md"><b>Setup</b></a> ·
  <a href="VERSIONING.md"><b>Versioning</b></a> ·
  <b>Changelog</b> ·
  <a href="LICENSE.md"><b>License</b></a>
</p>

</div>

---

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
  <suite>` 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
  <srcdir> <outdir>` 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 `// <file>:<line>` 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 <file.flash>` 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 <file.flash>` 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 <file>` 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/<name>` (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)