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