Quartz v5.25

Handoff — Binary DSL Phase 2 Track B SHIPPED

Head: f1f82b6d on trunk. Fixpoint stable at 2111 functions. Binary DSL test count: 112 (93 prior + 19 new). All green.

Phase 2 is now FULLY SHIPPED — Tracks A (computed fields), C (arrays), and B (discriminated unions) are all in trunk with real bidirectional codegen.

What shipped in Track B (3 commits)

CommitSTEPWhat
f38e3e49B1Parser: match IDENT / N => { ... } end inside binary blocks. 2 new AST kinds (NODE_BINARY_UNION=98, NODE_BINARY_UNION_ARM=99). 6 parser tests.
5105cc78B2Typecheck: flat-union struct registration + 4 new error codes (QZ0954–QZ0957). 7 typecheck tests.
f1f82b6dB3+B4+B5MIR plumbing (9 new parallel tables) + PACK/UNPACK codegen + TCP-options roundtrip spec. 6 end-to-end tests. New ParseError variant InvalidDiscriminant (tag=3).

Surface

type TcpOption = binary {
  kind: u8
  match kind
    0 => { }                           # END_OF_LIST
    1 => { }                           # NOP
    2 => { mss: u16be }                # MSS option
    8 => { tsval: u32be; tsecr: u32be }  # Timestamps
  end
}

TcpOption decodes via TcpOption.decode(bytes) returning Result<TcpOption, ParseError>. Arm fields are flattened into the outer struct (v1 design); got.mss / got.tsval / got.tsecr are accessible alongside got.kind. Fields for inactive arms are zeroed.

Encode:

  • TcpOption { kind: 2, mss: 1460, tsval: 0, tsecr: 0 }.encode()[0x02, 0x05, 0xB4] (3 bytes).
  • Unknown discriminator at encode time is tolerated — just the prefix is emitted. Decode is strict (sees InvalidDiscriminant).

Decode error branches:

  • Err(UnexpectedEof) — buffer shorter than prefix, or shorter than prefix + matched arm’s byte length.
  • Err(InvalidDiscriminant) — discriminator value doesn’t match any arm. Unit variant in std/binary.qz::ParseError.

v1 scope / out-of-scope

In:

  • Fixed-width prefix (any mix of byte-aligned + sub-byte ints, including straddle).
  • Arm fields are byte-aligned integer primitives (u8/u16/u32/u64 with explicit le/be suffix). Endianness per field.
  • Empty arm bodies ({ }) for TCP-NOP-style zero-byte variants.
  • Cross-arm field-name reuse with identical wire spec (merges into one struct slot). Conflicting specs → QZ0957.
  • One union per block.

Out (future extensions; MIR tables already carry the info, code path would just need lifting):

  • Sub-byte arm fields (e.g. match kind / 1 => { flags: u4; pad: u4 }).
  • Variable-width arm fields (pstring, cstring, bytes, arrays).
  • Variable-width prefix fields before a union.
  • Multiple unions per block.
  • Implicit-enum sugar for the decoded value (currently flat struct). The handoff doc discussed this under STEP B2 — it’s a pure ergonomics layer on top of the existing MIR metadata and can land later.

Files touched

  • self-hosted/frontend/node_constants.qz — 2 new NODE kinds
  • self-hosted/frontend/ast.qz — ast_binary_union + ast_binary_union_arm
  • self-hosted/frontend/parser.qz — ps_parse_binary_union + call-site in ps_parse_binary_block_body
  • self-hosted/middle/typecheck.qz — _tc_bin_push_field dedup helper + union-aware tc_register_binary_block_def + validator
  • self-hosted/backend/mir.qz — 9 new MirProgram tables + mir_register_binary_union + 6 accessors
  • self-hosted/backend/mir_lower.qz — mir_collect_binary_layouts strip-union-to-side-tables + mir_collect_structs flatten-arm-fields
  • self-hosted/backend/cg_intrinsic_binary.qz — _cg_bin_emit_pack_union + _cg_bin_emit_unpack_union + 3 helpers
  • std/binary.qz — new InvalidDiscriminant variant
  • spec/qspec/binary_union_parse_spec.qz (6 tests)
  • spec/qspec/binary_union_typecheck_spec.qz (7 tests)
  • spec/qspec/binary_union_spec.qz (6 end-to-end tests)

Pre-existing bug discovered (NOT a Track B regression)

to_str() crashes (SIGSEGV) on integer values ≥ ~300 million when used inside string interpolation. Verified pre-existing — reproduces with puts("x=#{0x12345678}") or puts("x=#{305419896}"). Smaller values work, direct access (e.g. puts(to_str(x)) with x = 0x12345678) also works. Only #{} interpolation with large ints crashes. Filing for next session as a P1 compiler bug. Track B roundtrip tests use small values (100/200) to dodge this.

Next session options

  1. Phase 2 Track B v2 — lift the arm-field scope restrictions. Sub-byte arm fields and pstring/bytes/arrays inside arms. The MIR tables already carry spec + width; the arm emitter just needs to call the variable-path helpers instead of inline byte loops.
  2. Implicit-enum sugar for union decode values. Currently arm fields are flat on the outer struct; an implicit enum would make match opt / Mss(mss) => ... ergonomic. Purely additive — the wire layout is untouched.
  3. Phase 3 dogfood. Migrate compiler internals to the DSL — intmap header, channel layout, Future state machines, MIR-instruction encoding, AST-node layout. All the groundwork is now stable.
  4. Fix the to_str interpolation bug. P1 compiler bug filed above.
  5. Kernel Epic (SYS.1 atomic RMW). Tier 6 unikernel/bare-metal work from the April 17 planning session.

Safety rails

  • quake guard mandatory before every commit — pre-commit hook enforces it. Do not --no-verify.
  • Smokes (brainfuck + expr_eval) after every guard. Both green at head.
  • Fix-specific backup from this session: self-hosted/bin/backups/quartz-pre-binary-phase2-trackb-golden.
  • Full QSpec not safe in Claude Code PTY — use quake qspec_file FILE=... for targeted runs.