Overnight Handoff — Binary DSL Phase 2 Track A landed; B and C open
Baseline: 801ed0c5 on trunk (Phase 2 Track A — computed fields — shipped).
Fixpoint: 2088 functions (was 2087 before Track A).
Tests: 86 binary-DSL green (80 prior + 6 new in binary_computed_spec.qz).
Design doc (canonical): docs/design/BINARY_DSL.md — still the locked 12 decisions.
Prior handoffs (read for context if anything’s unclear):
overnight-binary-dsl-phase-2-kickoff.md— the prompt that set up the three tracks (A/B/C).overnight-binary-dsl-phase-1-5-kickoff.md— shipped all 5 worked examples.- Earlier phase-1 handoffs have D1–D15 discoveries.
What shipped this session (1 commit, 801ed0c5)
Track A — Computed fields. name: type = <expr> inside binary {} blocks. On .encode() the expression is evaluated fresh with self bound to the user-supplied struct, and the result overrides whatever value the user passed at construction. On .decode() the field is read from the wire like any other (v1 trusts; v2 may validate).
type Pkt = binary {
magic: u16be = 0xcafe
counter: u8
doubled: u8 = self.counter * 2
payload: bytes
}
Spec: spec/qspec/binary_computed_spec.qz — 6 tests covering constant override, prior-field self.*, later-in-block self.*, multi-field arithmetic, computed + variable-width tail, repeated-encode freshness.
D16 — Clone-and-mutate architecture for computed fields
The pack path was kept unchanged. Instead of extending PACK to take per-field scalar overrides, MIR-lowering of .encode() detects computed fields and builds a fresh clone carrying overrides. Codegen sees a normal struct and emits the usual load/store prefix sequence.
Why this is the right call:
- Zero codegen churn.
cg_emit_binary_packand its prefix helpers are unchanged. - Expression lowering reuses the normal MIR pipeline (function calls, arithmetic, closures — whatever the user writes Just Works).
selfis bound viamir_ctx_bind_var("self", slot)+mir_ctx_mark_struct_var("self", type)+mir_emit_store_var("self", val), then torn down via vec-size restore on the bindings / struct_types lists. Shadowing-safe.- Cost is one
malloc(N * 8)+ N loads/stores per encode. Fine for v1; v2 optimization (elide clone when user’s value is unused) is an easy follow-up.
Registry extension: MirProgram.binary_field_compute_asts: Vec<Int> is parallel to the existing name/spec/width vectors. 0 = not computed; otherwise AST node id of the expression. Populated from ast_get_extra(fnode) in mir_collect_binary_layouts.
Parser change: ps_parse_binary_block_field now accepts optional = <expr> after the type. The expression AST handle is stashed in the field’s extras slot (free on NODE_BINARY_FIELD today).
Track A restrictions (filed as v2 follow-ups if a consumer asks)
self.nameis mandatory for prior-field references. Barecounterwon’t resolve — no implicit-receiver scope yet. File as TA-F1 if a user complains.- No decode validation. If a peer sends a packet with the wrong computed value, we pass it through. Validation at UNPACK time is in the Phase 2 design but out of Track A’s scope — file as TA-F2 when a consumer needs it (typical ask: TCP checksum rejection).
- Computed fields are still user-constructible.
Pkt { doubled: 0, ... }accepts the user’s value even though.encode()overrides it. Making the constructor elide computed fields would need struct-literal typecheck cooperation — file as TA-F3. - One clone per encode. No liveness analysis to elide the copy. File as TA-F4 (perf, not correctness).
Copy-paste handoff prompt (paste into a fresh session)
Read docs/handoff/overnight-binary-dsl-phase-2-track-a-done.md FIRST.
The earlier phase-2-kickoff has the full Tracks B / C descriptions —
Track A is done (computed fields); pick B or C next.
Starting state (verified at handoff 801ed0c5):
- Trunk clean. Guard stamp valid at 2088 functions. Smoke green.
- 13 binary-DSL specs, 86 tests, all green.
- Session backup from Track A is quartz-pre-binary-phase2-golden; create
a new fix-specific backup before any compiler work:
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-binary-phase2-trackX-golden
(Substitute trackb / trackc.)
NEVER overwrite the quartz-pre-binary-phase2-trackX-golden snapshot until
the last STEP of the new session is committed AND smoke + targeted specs
pass. The rolling quartz-golden managed by `quake guard` gets overwritten
on every successful build — the fix-specific copy is your escape hatch.
Pick ONE of the two remaining tracks and ship it clean. Don't start
the other until the first is committed end-to-end with tests.
TRACK B — Discriminated unions inside binary {} (higher impact).
Surface (proposed, see BINARY_DSL.md):
type Tcp = binary {
data_offset: u4
flags: u8
...
options: [TcpOption] # uses Track C [T] form
}
type TcpOption = binary {
kind: u8
match kind
0 => { } # END_OF_LIST, no body
1 => { } # NOP
2 => { mss: u16be } # MSS
8 => { tsval: u32be; tsecr: u32be } # Timestamps
end
}
Semantics:
- Discriminator is always the FIRST field, is a primitive integer.
- Each variant adds additional field(s) after the discriminator.
- Decode reads discriminator, dispatches to variant layout.
- Encode: the Quartz value is an enum with the discriminator baked
in; pack writes discriminator + variant body.
Scope: parser (match inside binary block), typecheck (variant type
registration), MIR (new opcode OR extend PACK/UNPACK with a variant-
dispatch indirection), codegen. Commit the new enum + variant-aware
prefix emitter as a coherent chunk.
Size estimate: 800-1200 lines.
Spec: spec/qspec/binary_union_spec.qz — TCP options, PE section
kinds (.text / .data), ELF section header types.
TRACK C — Array forms [T; n] / [T; field] / [T] (primitive elements).
Surface (parser already accepts):
type DnsQuery = binary {
id: u16be
flags: u16be
qdcount: u16be
questions: [u8; qdcount] # count-prefixed
rest: [u8] # rest-of-stream
}
type UuidSlot = binary {
uuid: [u8; 16] # fixed-length
}
Semantics (primitives only in first pass — nested binary blocks in
element positions are a Phase 2d follow-up):
- Struct field presents as Vec<Int> to the user (all numeric
primitives map to Int).
- PACK: loop N times, encode each element as its primitive type
at current cursor.
- UNPACK: allocate Vec, loop N times, decode primitive, push.
- [T; n] — N known at codegen time.
- [T; field] — N loaded from prior struct field slot at runtime.
- [T] — N computed from (remaining_bytes / element_size).
Scope: typecheck (_tc_bin_field_annotation should return "Vec<Int>"
for array specs — currently returns "Int" placeholder), codegen
(add classes -10..-12 to _cg_bin_var_spec_class, extend both
pack_variable and unpack_variable tail loops).
Size estimate: 200-400 lines.
Spec: spec/qspec/binary_arrays_spec.qz.
Recommendation: Track B is higher impact per the kickoff doc. Track C
is the smaller scope and fills a Phase 1 gap (the only Phase 1
deliverable left out). Pick based on session energy / appetite.
Workflow per STEP (identical to prior phases):
1. Write QSpec tests FIRST (red phase) — failures must be specific.
2. Implement the minimum to green.
3. Run `./self-hosted/bin/quake guard` before EVERY commit.
4. Smoke after every guard — brainfuck, expr_eval (both in ~10s).
5. Commit each STEP as a single coherent commit.
Prime Directives v2 compact:
1. Pick highest-impact, not easiest.
2. Design is locked (BINARY_DSL.md) — implement, don't redesign.
3. Pragmatism = sequencing correctly; shortcut = wrong thing.
4. Work spans sessions; don't compromise because context is ending.
5. Report reality. Partial = say partial.
6. Holes get filled or filed.
7. Delete freely. Pre-launch.
8. Binary discipline: guard mandatory, smokes + backups not optional.
9. Quartz-time = traditional ÷ 4.
10. Corrections = calibration, not conflict.
Stop conditions:
- Track complete with fixpoint stable → write next handoff.
- Blocked on compiler bug → file in Discoveries, commit what works.
- Context limit → stop at next clean commit boundary, write handoff.
Pointers (verified 2026-04-17 post-Track-A):
- binary field AST: `NODE_BINARY_FIELD.extras` holds computed expr
AST handle (Track A uses this slot). Leave untouched for B/C if
possible — or carve out a second slot explicitly.
- MIR layout registry: add parallel Vec<Int> slots alongside
binary_field_{names,specs,widths,compute_asts}. `mir_register_binary_layout`
takes parallel vecs; pass yours too.
- `_cg_bin_var_spec_class` returns -99 for arrays today. That's the
hook-in point for Track C. Classes are negative ints; next free is
-10 and below.
- Fixed-prefix emit helpers (`_cg_bin_emit_pack_prefix_stores` /
`_cg_bin_emit_unpack_prefix_reads`) handle straddle + sub-byte +
byte-aligned. Extend them, don't inline new codepaths.
- Variable-tail pack emitter around line 761 in cg_intrinsic_binary.qz;
unpack around line 989. Both loop over [split..field_count) and
dispatch on spec class.
Test status after Track A
| File | Tests | Status |
|---|---|---|
binary_parse_spec.qz | 14 | 🟢 green |
binary_typecheck_spec.qz | 19 | 🟢 green |
binary_mir_spec.qz | 10 | 🟢 green |
binary_types_spec.qz | 5 | 🟢 green |
binary_methods_spec.qz | 3 | 🟢 green |
binary_bitcast_spec.qz | 3 | 🟢 green |
binary_with_spec.qz | 3 | 🟢 green |
binary_roundtrip_spec.qz | 5 | 🟢 green |
binary_varwidth_spec.qz | 5 | 🟢 green |
binary_straddle_spec.qz | 3 | 🟢 green |
binary_eof_spec.qz | 4 | 🟢 green |
binary_strict_spec.qz | 6 | 🟢 green |
binary_computed_spec.qz (new) | 6 | 🟢 green |
| Total | 86 | 🟢 green |
Smokes (run after quake guard at end of session): examples/brainfuck.qz + examples/expr_eval.qz both pass.
Full QSpec suite NOT run from Claude Code (CLAUDE.md protocol). Run ./self-hosted/bin/quake qspec in a terminal to catch any cross-spec regression before declaring Track A “fully done.”
Safety rails (verify before starting Track B or C)
- Quake guard before every commit. Pre-commit hook enforces it.
- Smoke after every guard. brainfuck + expr_eval are enough.
- Fix-specific backup at
self-hosted/bin/backups/quartz-pre-binary-phase2-trackX-golden(create at top of next session). - Full QSpec NOT in Claude Code. The harness PTY can hang on large runs. Use targeted
FILE=...invocations for spec files. - Crash reports first (CLAUDE.md): on silent SIGSEGV check
~/Library/Logs/DiagnosticReports/quartz-*.ipsbefore ASAN/lldb.