TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    Design: match on scalar values

    Status: decided 2026-08-03 — scalar match on int/char/bool, with inclusive ranges, sets, and _. An ergonomics feature, not a speed feature; Phase 2 measured −1.8% / −2.4% vs the if chain.

    The demand, measured

    match is enum-only, so every dispatch table in the tree is an if/elif ladder. Six files carry a ladder of 4+ arms over a scalar:

    filearmssubject
    tools/lsp.ty55byte values from string indexing
    tools/tycho-vm/main.ty38opcodes (24-opcode dispatch, :622)
    tools/tychofmt.ty19byte values
    corelib/json/json.ty18byte values (corelib/json/json.ty@parse_value)
    corelib/markdown/markdown.ty16byte values
    tools/tycho-q/main.ty9byte values

    Two shapes the arms actually need, both already written as if:

    Every ladder subject is int: string indexing returns the byte value as an int (src/tychoc.c:1902@T_BYTES), so the char-position ladders all compare ints. No ladder in the tree dispatches on string or bytes.

    How match works today

    Decisions

    D1 — Which types. int now; char and bool fall out free; string/bytes deferred; float refused

    D2 — _ and exhaustiveness. _ is REQUIRED for int/char; optional for bool

    An int match without _ cannot be exhaustive and is a compile error — the same fail-closed stance the resolver already takes on enums (tests/reject/match_non_exhaustive.ty). The emitted runtime abort stays as the backstop (it becomes unreachable: the resolver proves coverage). For bool, true: + false: covers the domain, so _ is optional, exactly like the enum rule.

    Reason: enums are exhaustiveness-checkable by construction; ints are not. A non-exhaustive scalar match would be a new silent-fall-through failure mode — the thing the repo's reject suite exists to refuse (json's "cannot spin" is the same instinct).

    D3 — Duplicate arms are an error, and the rule extends to overlap

    A repeated literal is an error, as a repeated variant already is. Two arms whose patterns overlap are also an error: 1..9 plus 5, or 1 | 3 plus 3. Overlap detection is interval arithmetic over the arm patterns (merge sorted intervals; any collision fails, naming both arms).

    Reason: a duplicate makes an earlier arm dead and the match ordering-dependent — the bug a match exists to make unexpressible. Ranges make this subtle, so the resolver must check it rather than the writer.

    D4 — No fall-through

    One arm runs; arms are blocks (statement form) or value expressions (value form, §13.5) — unchanged from enums. Fall-through is the classic source of the exact bugs match removes, and range arms make it unnecessary: the range IS the group (the VM's arithmetic group becomes one OP_ADD..OP_GE arm instead of seven labels with a shared body).

    D5 — Ranges and sets. Both: 1..9 (inclusive) and 1 | 3 | 5

    D6 — Codegen. Emit a C switch for int/char arms when narms >= 4; a chain otherwise; unrolled ranges

    What it would buy — the arithmetic

    From the plan's own measurements (docs/internals/plan-tycho-vm-DONE.md:36-38): VM throughput is 1.39 M instructions/sec — about 720 ns per instruction — and the dispatch is ~12 int comparisons per instruction (docs/internals/plan-tycho-vm-DONE.md:54). An int compare plus branch is ~2 cycles, ~0.7 ns at ~3 GHz, so the whole dispatch chain is ~8 ns ≈ 1% of the per-instruction budget. A jump table replaces it with a bounds check plus an indirect jump, ~2–3 ns — a saving of ~0.5–1% of VM time, inside measurement noise.

    The dominant cost is elsewhere: a struct-with-a-string copy is 815 ns against 110 ns for a bare JMP (docs/internals/plan-tycho-vm-DONE.md:36) — 7×. Copies and arena traffic, not comparisons, are what the VM spends its 720 ns on.

    Conclusion, stated plainly: scalar match is an ergonomics feature with fail-closed semantics, not a speed feature. It turns 12-comparison ladders into readable, dup-checked, exhaustiveness-checked arms. Phase 2 must measure the VM's instructions/sec before and after and report both; the prediction is flat within noise, and a change of more than ~2% would itself be a surprise worth explaining.

    Measured at Phase 2 (2026-08-03). The old if/elif dispatch (built from git HEAD) vs the scalar match, same compiler, same workload: 300 fib runs, best of 3 — 2.354 s → 2.311 s (-1.8%); 500 sort runs, best of 5 — 0.616 s → 0.601 s (-2.4%). Marginally faster than the chain, consistent with the switch lowering; the estimate's ~1% dispatch slice was a little low because a 24-way comparison chain mispredicts more than the naive 2-cycles- per-compare figure assumed. Neither number is a reason to build for speed.

    What Phase 2 touches

    Done-when (Phase 2's gate)

    1. tools/tycho-vm/main.ty:524's dispatch is written as a scalar match, with the grouped arithmetic arm spelled OP_ADD..OP_GE.
    2. corelib/json/json.ty@parse_value's byte dispatch is written as a scalar match.
    3. Both still pass their gates (make test, make corelib, make vm-check), and the VM's instructions/sec is measured before and after and reported.

    Edge cases, settled