← board

Zig frontend — a working SKELETON; 6 of 28 ordinary constructs compile (re-measured 2026-09-12)

Theoretic completion (2026-07-08, Track Z — user-directed)

Sub-tickets 3/4/5/7 landed in one pass, still zero shared-internals edits (zparser.inc + one lexer char + test + Makefile). The trick throughout: every "missing primitive" was faked with vocabulary the IR already has — the same UClass tag+payload shape the Rust skeleton's enums pioneered:

Test test/test_zig_advanced.zig (switch/defer/errdefer/optionals/error propagation/slices in one program) wired into make test. Gate: all three Zig tests green, testmgr quick 11/11, self-host fixedpoint byte-identical.

Probe verdict: still no shared-internals bug — AN_TERNARY, AN_DEREF on computed addresses, AN_ADDR over AN_INDEX, deep nested AN_SEQ/AN_IF chains from desugaring all worked as documented. One self-inflicted lesson worth recording: the paramless-recursion pitfall (bare ZParseStatement reads the Result alias instead of recursing) bit AGAIN inside the defer dispatch — third frontend it has bitten; see frank2-paramless-name-semantics.

Sub-ticket #6 landed too (2026-07-09, type-param subset)

After the user pointed out Pascal generics were mostly parser-side, #6 followed the same route: fn f(comptime T: type, ...) monomorphized by token-buffer substitution (rparser's RSpecializeGenericFn ported as ZSpecializeGenericFn — Zig is easier: the type arrives explicitly as the first call argument, no inference). Mangled f$i64 specializations, cached; comptime param dropped from the copy; mid-stream jmp bracketing reused; two Zig-specific hazards handled (caller's defer stash and CurProc/FrameSize saved around the nested compile). Explicitly NOT a comptime VM: no comptime values, control flow, or builtins.

What remains is exactly the non-frontend work (parked, would upscale to Track A only on its own merits per experimental/README.md):

Sub-ticket 2 landed (2026-07-07, Track Z — user-directed scaffolding pass)

zig-structs-and-pointers done, purely frontend-side as the gap map predicted (zparser.inc + test + Makefile only; zero shared-internals edits):

Test test/test_zig_structs.zig wired into make test. Probe verdict: again no shared-internals bug — record/pointer/array lowering worked first try; self-host fixedpoint byte-identical. Cross-target matrix offloaded to Track T per the watcher protocol.

Skeleton landed (2026-07-06, Track Z)

Sub-ticket #1 (zig-frontend-skeleton) is done, mirroring the Ada-skeleton precedent exactly (additive only: compiler/zlexer.inc + compiler/zparser.inc new files, isZig flag + .zig dispatch in compiler.pas, one-line isZig var in defs.inc — no new AST nodes, no new IR, no backend work). zparser reuses rparser.inc's node helpers (RSeqAppend/RMakeIdent/RBinOp/RWiden), so the two C-family skeletons share plumbing.

Landed subset: top-level [pub] fn (≤4 scalar params, i8–u64/usize/ isize/bool/void), calls + recursion, var/const with type inference and = undefined, assignment + compound (+= etc), if/else-if/else, while with : (continue-expression), range for (lo..hi) |i| (exclusive hi, per Zig), break/continue, and/or, _ = expr; discard, const std = @import("std"); header swallowed, and std.debug.print("fmt", .{args}) lowered onto AN_WRITE/AN_WRITELN with {}-placeholder splitting (segments point straight into the fmt token's TokChars — no copies). Test: test/test_zig_skeleton.zig wired into make test next to the Ada one.

Probe verdict: no shared-internals bug found this pass — shared pipeline (AN_* → IR → x86-64) handled the Zig shapes first try. Both bugs hit while building were frontend-local: (1) Zig integer / initially mapped to tkSlash = Pascal REAL division → double bits reinterpreted as int64 garbage; fixed by lowering / to tkDiv (trunc div) since all skeleton types are integers — note rparser.inc still maps Rust / to tkSlash, same latent hazard, its tests just never divide ([[feature-rust-frontend]], worth a one-line note there); (2) a { past } } comment-brace typo in my own code.

Known deviations (accepted, documented in zparser.inc header): continue inside while ... : (expr) skips the continue-expression (Zig runs it); const mutability not enforced; no overflow safety; {{ }} fmt escapes unhandled; x86-64 only.

Reframed under the esoteric-frontend-probe category (2026-07-05)

Parked as a full-language effort (below), but a skeleton-only pass (lexer/ parser for a trivial subset, lowering onto existing IR, no comptime engine) is back in scope — not to make Zig usable, but as a bug-probe against shared internals. See [[feature-esoteric-frontend-probes]] for the category rule.

Why parked (2026-07-05, user decision — rationale corrected 2026-07-05)

Original log entry claimed comptime's recursive/Turing-complete evaluation "undermines determinism" and conflicts with the byte-identical fixedpoint self-host gate. That claim was wrong and is withdrawn. Recursion and Turing-completeness threaten termination (an unbounded comptime loop — hence Zig's own branch-quota limiter), not determinism: a pure interpreter with no access to wall-clock/randomness/filesystem/mutable global state gives the same output for the same input regardless of recursion depth. And the fixedpoint gate is about the PXX compiler itself (written in Pascal) reproducing byte-identically across bootstrap stages — a Zig frontend's comptime interpreter only runs while compiling Zig source, never during the compiler's own self-compile, so it cannot threaten a gate it is never part of.

Actual reason to park: pure engineering scope, not architectural risk. comptime is pervasive (not a corner feature — no separate generics syntax; generics, @TypeOf, conditional compilation, and most of std all lean on it), and the builtin surface is large (@import, @sizeOf, @ptrCast, @field, …, each comptime-typed) with no "ignore and link" escape hatch the way C headers give other frontends. See "Notes on scale" below — that section already had the honest framing; the determinism paragraph was an unnecessary and incorrect addition on top of it.

Decision: scope an Erlang frontend first — see [[feature-erlang-frontend-scoping]], on its own merits (different engineering domain: runtime/scheduler work vs. a compile-time interpreter), not because it is "more deterministic" — Erlang's actor-model preemptive scheduling is itself a classically nondeterministic runtime execution model (message arrival order, process interleaving), so determinism was never a valid axis to rank these two on. Revisit Zig if a comptime-style engine becomes independently justified by something else needing it.


Original scoping (2026-07-03, pre-park)

Motivation

Add a Zig-syntax frontend (5th, after Pascal / Nil-Python / C / Rust-planned) lowering to the existing shared IR/backends, same shape as the C frontend's v80 bring-up (clexer.inc/cparser.inc/cpreproc.inc → shared IR). Zig is C-style / systems: manual memory (explicit allocators, no GC), structs, pointers, plain integers, C-like control flow — so its type theory maps onto the current statically-typed IR directly, much like C, with no ownership/move/trait system to model (simpler than Rust in that dimension). But "simpler type theory" does NOT mean trivial: reaching a subset that compiles real Zig is likely non-trivial because comptime is pervasive and the builtin surface is large — see "Notes on scale" below. It is the more tractable of the two new-frontend requests (JS parked as architecturally out-of-scope, see [[feature-js-frontend-parked]]), not an easy one.

Explicit non-goals (scope cuts up front, like the C/Rust precedent)

What already exists to reuse (confirmed reusable machinery)

Much of the Rust RTL/type groundwork (tagged unions, slices, drop/defer) is directly shared with Zig — sequencing the two together would amortize it.

AST/IR gap map — "pure frontend" vs "needs shared internals" (2026-07-04)

Verified against the current node/type inventory (defs.inc) + empirical checks.

Lowers onto EXISTING AST/IR — a skeleton subset is pure lexer+parser, zero Track-A internals change:

Zig construct Reuses today
fn / params / return procs
i8..i64 / u8..u64 / usize / isize tyInt8..Int64 / tyUInt8..UInt64 / tyNativeInt / tyNativeUInt (1:1)
if / while / for / switch-on-int / blocks AN_IF / AN_WHILE / AN_FOR / AN_SWITCH
struct + field access tyRecord / AN_FIELD
*T / &x / .* tyPointer / AN_ADDR / AN_DEREF
[N]T + indexing arrays / AN_INDEX
plain enum existing enum table (verified Ord)
const x = expr / var x = expr inference tyAuto inline-var inference
value-exprs (a ? b : c-ish, ++, comma) AN_TERNARY / AN_INCDEC / AN_COMMA (from C frontend)

Needs NEW shared machinery (Track A internals — NOT lexer+parser), and each overlaps an already-planned Rust ticket:

Zig construct Missing today Shared with
error unions E!T, union(enum) generalized tagged union (only tyVariant 16-byte scalar + exception-match exist; no struct-payload tagged union) [[feature-rust-match-enum-payload]]
slices []T / []const u8 no tySlice; a ptr+len non-owning view type [[feature-rust-borrowed-slice-type]]
optionals ?T ?*T = free (nullable ptr); ?i64/?struct need has-value+payload (part tagged-union) (tagged-union)
try / catch / orelse propagation lowering over the above
defer / errdefer MINOR: no AN_DEFER, but AN_TRY_FINALLY + IRLowerCleanupToDepth exist to desugar onto [[feature-rust-drop-move-tracking]]
comptime + builtins (@import/@TypeOf/…) biggest semantic gap — no comptime VM (subset out of v1)

Bottom line: skeleton (#1) = pure frontend, shippable, zero internals risk. A useful subset = skeleton + the 3 shared additions (tagged-union, slice, optional) — which ARE the Rust Track-A tickets. Zig and Rust share their hard parts; building either advances the other. The "auto typing" is on the free side; the tagged union is the real cost.

Sub-tickets (split when work starts — don't flood the board yet)

Track A (compiler internals — shared AST/IR/symtab/backends):

  1. zig-frontend-skeletonzlexer.inc/zparser.inc, entry point (.zig dispatch in compiler.pas), minimal subset: fn, pub, integers, var/ const, if/while/for/switch, blocks, return, basic operators. Gates everything else. Mirrors C-frontend skeleton scope.
  2. zig-structs-and-pointersstruct, field access, *T/*const T, &x, .* deref, [N]T arrays.
  3. zig-optionals-and-error-unions?T, E!T, orelse, catch, try, if (opt) |x| capture, unreachable. Depends on the generalized tagged-union primitive (shared with [[feature-rust-match-enum-payload]]).
  4. zig-slices[]T/[]const T ptr+len views, slicing a[lo..hi], .len/.ptr. Shared with [[feature-rust-borrowed-slice-type]].
  5. zig-defer-errdefer — scope-exit + error-path-exit execution over the existing cleanup machinery.
  6. zig-comptime-genericsfn f(comptime T: type, ...) via monomorphization; comptime-known sizes. NOT a general comptime VM.
  7. zig-switch-and-tagged-enumswitch on enums/tagged unions with payload capture; enum/union(enum).

Track B (lib/zigrtl):

  1. zig-rtl-core — allocators as thin wrappers over the pxx heap (std.heap.page_allocator-shape), std.debug.print / std.log wired to existing write machinery, std.mem basics (copy, eql, span).

Notes on scale — Zig is NOT an easy source (2026-07-03, user flag)

Earlier framing ("smaller lift than Rust") is only half true and needs tempering. Zig has no ownership/move/trait system, so its type theory is simpler than Rust's — but reaching a practically useful subset (one that compiles real Zig) is likely NON-trivial, for reasons specific to Zig:

Net: comparable-or-harder than the C frontend's multi-session bring-up to reach "compiles real Zig", despite the simpler type theory — the cost is comptime + builtins, not the type system. #1 gates everything; #3 (error unions/optionals) and #6 (comptime-generics) are the real gates on usefulness. Sequence with the Rust tagged-union/slice/drop work to share the primitives.

Log

5/6-param internal calls fixed (2026-07-16, Track Z / A)

Sibling-sweep of the Rust 5/6-param spill fix: ZParseTopLevelFn had the same bespoke case i of 0..3 param-register spill the Rust frontend did (the code comment literally said "byte-for-byte the sequence RParseTopLevelFn emits") — emitting no modrm byte for param index 4/5, so mov [rbp+off], r8 came out as 48 89 <off32> and SIGILL'd on the 5th param. Both frontends now share REmitParamRegSpill (computes REX.R for r8/r9); the Zig param cap is raised from 4 to the register-convention 6. test/test_zig_manyparams.zig (add5/add6 + 5-param recursion). Regressions green: all three prior Zig tests, quick tier, self-host byte-identical. Lesson reinforced: a bug copied byte-for-byte across sibling frontends must be fixed (or factored) across all of them — see the sweep-sibling-dispatch-branches rule.

Slice params + array literals + a real-load chess perft (2026-07-16, Track Z)

Two frontend enablers landed (zparser.inc only; self-host byte-identical):

Then the payoff — test/test_zig_chess_perft.zig, a full-legality chess perft compiled from Zig (the esoteric-frontend-probe's point: exercise the shared pipeline under real load). Slice params carry the board + move list, 5-param recursion drives the tree, array literals hold the offset tables, deep nested control flow runs movegen/make/unmake. Node counts are reference-exact: startpos perft(4)=197281, Kiwipete perft(3)=97862. Probe verdict: no shared-internals bug — the one bug found this pass was the frontend-local 5/6-param spill (fixed above, shared with Rust). Regressions green: all Zig tests, quick tier, self-host byte-identical.

Re-measurement 2026-09-12 (frankZ, Track Z) — the completion claim is stale

Prompted by the stale-hazard rule: an unranked experimental umbrella asserting completion is a warning that decays like a lock. Measured at 356d4d4fa with a freshly converged HEAD binary (808076de24be), CWD at the repo root. That binary is byte-identical to the one pin v408 ships (last.sha256 = 808076de24be..., pinned 2026-09-12 from tree 14934e60d; I built at 356d4d4fa, and only docs landed between — determinism, not the same tree), so this census describes exactly the compiler that is now $(PXX_STABLE). This used the HEAD-built compiler, NOT $(PXX_STABLE), so it is independent of pin age — and every gap below is a PARSE error, raised before any builtin or RTL is reached, which no pin could produce.

The existing suite is GREEN and that is not the same claim. All six test/test_zig_*.zig compile, run, and match their Makefile expected strings byte-for-byte, chess perft included. The frontend has not rotted. But the suite is written in the subset that works — it uses for (0..N) |i| in all four loop sites and never for (arr) |v|, and declares arrays only as [5]i64 = undefined. That is the passing-arrangement population, so it certifies the skeleton rather than probing it.

28 isolated single-construct probes, one file each (isolation on purpose: a combined program reports a first failure and hides the walls behind it; the scaffolding-only control compiles, so the failures are the constructs). 6 passbool, usize, pub fn, string literal + {s}, while (c) : (i += 1), and the control. 22 fail. Three first-round failures were CONFOUNDED and were re-probed before attribution: for (a) |v| died on the [_]i64{} literal, not on the loop. Disambiguated, [3]i64{1,2,3} compiles (the ticket claims exactly that and is correct); [_]T{} does not.

Grouped by MECHANISM, not by probe count — four gates produce most of it:

  1. The struct body is a flat token walk requiring ident : ident (zparser.inc:1611-1618). One shape constraint refuses methods, const Self = @This(), default field values, and every non-scalar field type (arrays, ?T, nested structs) — 7 probes, one cause. Methods fail regardless of position, so it is not an ordering bug.
  2. Top level accepts only fn/pub fn (zparser.inc:2111-2112) — no top-level const, no enum, no union(enum), no error{} set, no test block — 5 probes.
  3. for unconditionally parses a rangefor (arr) |v| reaches the shared range parser and errors expected '..' with no Zig: prefix, which is the tell that it never reached a frontend check.
  4. The type vocabulary is integers, bool, void and declared structs — the diagnostic says so in those words. No floats at all: f64 is unknown type.

Singles: switch ranges 0...3 (the ticket does say "No ranges"), comptime, labeled blk: { break :blk v }, unreachable, multiline strings, []const u8 parameters, and pub fn main() !void (refused by name).

What this changes for ranking. Of the gaps above, items 1, 2, 3, [_]T{}, labeled blocks and multiline strings need NO shared machinery — they are parser work in zparser.inc, which is Track Z's own file. The ticket's standing position, that what remains is Track A's comptime VM and real tySlice/tagged-union primitives, is true only of floats, ranges and comptime. The single highest-value item is (1): struct methods are how essentially all real Zig is organised, and the shape constraint that refuses them also refuses six other things.

Nothing was fixed here — this is a measurement, and the frontend stays low-prio experimental. lib/zrtl, which the header names as a Track Z file, does not exist on disk at all.