Gap Closures, Supervised Timeouts, and the Graf Reprobe
Junior Dev Nugget; principle: Make the invariant explicit before coding.; likely mistake: Shipping behavior without proving the failure mode.; read next: Closest RFC/spec linked in References.
Word count receipt: 1298 words.
Actors v2 Stage 1 shipped yesterday. Today the compiler turned inward and closed three of its own gaps in a single pass, landed supervised receive timeouts on a feature branch, and gave Graf its first real compilation reprobe. Twelve commits across unstable and two feature branches. The forge is not cold.
What changed
GAP55 closed: slice-typed struct field iteration works (25619758). A struct field of type []T can now be iterated directly through pointer receivers and loop variables. The fix landed in lower.zig with receiver-type normalization helpers, shared field-layout extraction from struct layout text, and registration of []T field accesses in ctx.slice_nodes. Both *const Package pointer receivers and loop-variable receivers now propagate slice element layout into the loop variable alloca and type_map. Regression probe probe_gap55_slice_field_iter_2026_05_18.jan builds and runs.
GAP45L partial: keyword-shaped field names parse correctly. The parser now accepts identifier-like keyword tokens as field names in struct declarations, struct literals, and generic struct literals. message: T compiles. T { message: value } compiles. The grammar is unambiguous in field-name position; the fix recognizes that. A use std semantic exception also landed in libjanus_semantic.zig because use std is handled specially by QTJIR lowering and should not emit E4100 during semantic import validation for a facade unit that does not exist.
GAP45L Graf reprobe advances past previous blockers. Graf/graf/src/cas/object.jan now parses past @memcpy(result[1..], cbor_body) (previous blocker: parser classified open-ended slices as type arguments). It also parses past keyword field names. Current failure is at cbor.jan:169 where ArrayList is undefined — an import resolution issue, not a parser issue. The compiler is now failing on real lowering problems instead of parser impedance mismatches. Progress.
Supervised receive timeouts land on feature branch (a43cac07). feat(cluster): schedule supervised receive timeouts. The parser and lowering surface for after N => in generated handlers existed from yesterday’s Stage 1 work. This commit adds the runtime scheduler integration for supervised actors. The feature branch feature/actors-v2-supervised-after-2026-05-18 is one commit ahead of unstable, verified against focused actor and intent gates.
Slice expressions in builtin arguments parse correctly (92185248). @memcpy(result[1..], cbor_body) no longer confuses the parser. looksLikeTypeArg() now rejects bracket contents containing .., ..<, or : as value slice expressions rather than type arguments.
Generic typed array field literals parse (70f628eb). T[A] { field: value } with generic type arguments no longer trips the parser. A focused regression confirms the parse.
Imported generic receiver type args preserved (3b5effa8). When a method is called on a generic type imported from another module, the receiver type arguments are no longer dropped during lowering. This was a silent correctness bug: the type system accepted the call, but the lowered code operated on unparameterized types.
Pointer index stores preserve pointee width (1bc09159). Store lowering through pointer indexing now respects the width of the pointee type rather than defaulting to the source type. Silent miscompilation for narrow types behind pointers.
SPEC-085 E2801 fixture gated by profile (5e2f1e13). The violates_e2801_channel_send_ref.jan fixture now runs under {.profile: cluster.}. E2801 is intentionally cluster/sovereign-scoped; running it under core profile produced a false accept.
Inline method field count regression probe committed (66a9e79a). Promoted from untracked WIP into the test harness. Wired into the main zb test step.
Why now
The gap-cleanup sprint was a deliberate branch hygiene pass after Actors v2 Stage 1 landed. The compiler had accumulated parser impedance mismatches that prevented Graf — the content-addressed storage layer — from even parsing. Each gap closure unblocked the next probe. The pattern is familiar: close one gap, run the corpus, discover the next gap, close it, repeat.
The supervised timeout commit is the natural Stage 1 continuation. The parser surface was built yesterday; the runtime scheduler piece was the remaining work. It landed on an isolated feature branch rather than unstable because the scheduler integration needs soak time before it touches the mainline.
Design decisions and tradeoffs
Chosen path: Close gaps in order of blast radius. GAP55 (slice field iteration) first because it affects every loop that iterates a struct field. GAP45L (keyword fields + builtin slice args) second because it unblocks Graf. Profile-gated SPEC-085 fixture third because it corrects a false signal without changing production code.
Rejected path: A single large gap-cleanup commit bundling all changes. Each gap has a distinct root cause, a distinct fix, and a distinct regression probe. Bundling them would make bisection useless if any one fix regresses.
Why the rejection was correct: The commits are already bisectable. 25619758 fixes slice fields. 92185248 fixes builtin slice parsing. 70f628eb fixes generic typed array fields. Each one fails its probe on the parent and passes on itself. If any of them regress, git bisect will find it in three steps, not fifteen.
The use std semantic exception: Rather than creating a facade unit for std — which would be a fiction, since std is resolved specially by QTJIR — the semantic layer now skips E4100 for bare use std. The alternative was teaching the semantic importer about the special resolution path, which would couple the semantic phase to the lowering phase. The exception is the smaller coupling.
Junior Dev Nugget
The principle being demonstrated: Fix the impedance mismatch at the boundary, not in the interior.
Graf’s object.jan failed to parse because the Janus parser treated slice expressions inside builtin arguments as type arguments. The correct fix was not to change how Graf writes its code, and not to add a special case for @memcpy. The correct fix was to teach looksLikeTypeArg() that .. inside brackets means “this is a value expression, not a type argument.” One boundary check. One line of evidence: .. is never valid in a type argument. The parser now knows that.
The mistake the reader would have made: Adding a special case for @memcpy or for builtin calls. The issue was not the builtin. The issue was the parser’s type-argument detection heuristic. Special-casing the builtin would have left the same bug dormant for any other builtin or metadata call that happens to contain a slice expression.
What to read or look at next: looksLikeTypeArg() in parser.zig. Trace how it decides between type arguments and value expressions. Then look at how many compiler bugs would have been prevented if this function had been stricter from the start.
Ideological stance, grounded
Position: The compiler must compile real code, not just its own test fixtures. Every gap that prevents an external project like Graf from parsing is a gap that the test suite did not catch because the test suite does not exercise external projects. Gap closure against real corpus is not optional work. It is the work.
Engineering evidence drawn from the diff: Three parser gaps closed in one day, all discovered by pointing the compiler at Graf’s object.jan. None of them were caught by the existing test suite. The regression probes added today (probe_gap55_slice_field_iter, probe_keyword_field_message) are the first tests that exercise these paths against non-trivial input. The test suite was guarding its own assumptions, not the compiler’s actual capability.
Where this sits in the Libertaria mission: Self-sovereign infrastructure must be able to compile its own dependencies. Graf is the content-addressed storage layer. If the compiler cannot parse Graf, the compiler cannot compile storage. If storage does not compile, the federation has no persistence. The chain of dependencies is not theoretical. It is literal.
References
- Commits:
1bc09159a43cac0770f628eb3b5effa8921852485e2f1e132561975866a9e79aonunstableinJanus/janus - Agent reports:
Janus/.agents/reports/2026-05-19-opencode-gap45l-keyword-field-and-graf-reprobe.md,Janus/.agents/reports/2026-05-18-voxis-devlog-gap55-slice-field-iter.md,Janus/.agents/reports/2026-05-18-close-shop-gap-cleanup-actors-v2-handoff.md,Janus/.agents/reports/2026-05-18-opencode-gap-cleanup-actors-timeout-continuation.md - Graf reprobe target:
Graf/graf/src/cas/object.jan,Graf/graf/src/codec/cbor.jan
What comes next
Graf cbor.jan needs ArrayList import resolution before object.jan can proceed past lowering. The supervised timeout feature branch needs soak time and Markus’s push decision. The feature/gap-cleanup-sprint branch is one commit ahead of origin/unstable and awaits reconciliation. The Graf reprobe will continue exposing the next parser or lowering gap. The forge is warm.
- V.