fix(#505): fixed-length list patterns must match exactly n under --bytecode - #684
Merged
sunholo-voight-kampff merged 6 commits intoAug 12, 2026
Merged
Conversation
… and one operator The doc claimed a known root cause while leaving the fix site "unknown" across three packages, and cited the parent doc's verification log rather than verifying anything in place. A two-reviewer quorum blocked it twice on exactly that; both rounds were premise objections, so the controller measured them rather than forwarding them. Root cause is internal/gen/lower/match.go:lowerPatternCond, case *core.ListPattern: the empty pattern is special-cased to OpEq while every other list pattern gets OpGte, tail or no tail. internal/vm and internal/bytecode are not involved — neither contains a single Pattern reference; matching is lowered away in internal/gen. Verification log V-A..V-I added, all measured at 8ecebc0: - the bug reproduces (quicksort prints [3] under --bytecode, rc=0, no fallback) - a probe at the pinned site LANDS, BUILDS, and flips both the minimal repro and the flagship symptom to byte-identical-with-eval; restored byte-identical - V-H: isolated single-arm modules confirm the overflow bug independently at n=2 and n=3 and confirm underflow is already correct — V-B's ordered block let [x] intercept everything, which the round-2 quorum correctly refused to accept as proof - V-I: core.ListPattern is constructed in exactly two places, both in internal/elaborate/patterns.go, so Tail == nil <=> exact-length holds over the codebase and not merely over the type declaration Resolved via the narrow-refinement carve-out: both surviving objections asked for a measurement, neither disputed the design direction, and the milestone, AC numbering, estimate and scope are unchanged. Co-Authored-By: codex gpt-5.6-sol <noreply@openai.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three milestones: red-first AC2, the fix + AC1/AC3, then the mutation drills,
blast radius and gate sweep.
The planner refuted two things the controller handed it, both load-bearing and
both confirmed first-party before this commit:
- `go build ./...` is RED AT BASE (cmd/wasm only builds under GOOS=js), so the
mutation drill's "assert the mutant BUILDS" step could never have passed for
any mutation. Substituted `go build ./internal/... ./cmd/ailang`, rc=0 at base.
- the design doc's prescribed home for the fixtures, tests/golden/bytecode/, is
a Go test package (4 .go files, package bytecode_golden_test) that drives
pipeline->lower->compiler->vm in-process. It can never observe the CLI's
stderr, --caps IO or exit code, so AC1 and AC3 are unimplementable there.
Correct home is cmd/ailang/run_bytecode_test.go, which already shells out.
Planner also proved the AC2 fixture red at HEAD (exactly 3 diverging rows) and
green under the probe, which is what shows the fixture actually executes in the
VM rather than being silently bridged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AC2's table lives in cmd/ailang/run_bytecode_test.go, not tests/golden/bytecode/ as the design doc said: that directory is a Go test package driving pipeline->lower->compiler->vm in-process, so it can never observe the CLI's stderr, --caps IO or exit code, which AC1 and AC3 both require. The .ail fixture goes there; the test that runs it shells out. This commit is RED BY DESIGN — the fixture reproduces #505.
…tecode (M2) internal/gen/lower/match.go, lowerPatternCond, case *core.ListPattern emitted stmt.OpGte for every list pattern except the empty one, so [x] matched any non-empty list and silently discarded the tail. recursion_quicksort printed Quicksort: [3] instead of [1, 1, 2, 3, 4, 5, 6, 9] at rc=0, with no error and no fallback warning. The discriminator was already on the node: core.ListPattern.Tail. Tail == nil is a closed pattern and must be OpEq; Tail != nil (a ...rest pattern, or the :: cons form) genuinely means at-least-n and keeps OpGte. The old empty-list special case is just the Tail == nil, n == 0 instance of that rule, so it collapses into it. Verified by the controller outside the executor sandbox, through the real CLI: quicksort now prints the correct list, stderr has zero 'falling back to evaluator' lines (control: 'bytecode VM' present = 1), and isolated single-arm modules confirm overflow is now rejected at n=2 and n=3 while underflow was already correct. Fixes #505
Six drills, each asserted LANDED by sha256 and BUILDS by go build before its red was read, with the failing test and assertion text quoted rather than a bare exit code. The controller re-ran the load-bearing one itself: restoring unconditional OpGte LANDS (5142335d -> fd282d4d), BUILDS (rc=0), and reds both new tests with the right mechanism -- while the INVERSE ARM, the same mutant with those two tests -skip'd, is rc=0 over all of ./cmd/ailang. So the new tests are the sole killers and nothing else in the suite catches #505. That run also independently confirmed the executor's self-reported deviation: the plan prescribed 'if false && p.Tail == nil' as the drill mutant, which is NOT #505 -- it makes the empty pattern match everything too, so quicksort prints [] rather than [3]. The executor noticed and substituted the exact measured pre-fix lowering. Rule 3h(d): the self-reported deviation was better than the plan. Blast radius: seven examples MATCH across both engines; parity harness MATCH 151 -> 152, DIVERGE 6 -> 5, delta exactly recursion_quicksort.ail.
…ATOR
Rule 3j asks how many ways a mechanism can refuse; iter-75's dual asks how many
ways the forbidden thing can be spelled. Neither asks who decides what counts as
an input at all — and an enumerator's blind spot is invisible to every
downstream assertion by construction, so a full set of pinned branches, ten
mutations and a 93/100 evaluator can all agree while the input never arrives.
Two frictions, one per mission:
- mission-world iter-77 (proposed; World shares this skill but cannot edit it):
an allowlist gate with four mutation-killed refusal branches, defeated by
SNEAKY.AIL, because the enumerator is `find -name '*.ail'` and -name is
case-sensitive. rc=0, success line byte-identical to the pristine baseline.
- V1, corroborated first-party before adoption per the sibling-claim ghost
discipline, and a different mechanism — wrong SCOPE, not wrong case:
`make fmt-check-ail` enumerates `find examples stdlib -name '*.ail'` and
stdlib/ has never existed in this repo (the path is std/). Measured in one
call: 400 files as written, 446 with the correct root. 46 stdlib files sit
outside a gate that prints a green checkmark, and its empty-set branch is
itself a vacuous pass. Filed as #683.
Remedy: pair every enumeration with a deliberately widened control in the same
call and require the counts to agree, assert roots exist rather than reading
their emptiness, and give any enumerator-fed gate an anti-vacuity floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
sunholo-voight-kampff
deleted the
sprint/iter187-bytecode-pattern-arity
branch
August 12, 2026 21:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #505.
The bug
Under
--bytecode, a fixed-length list pattern matched any longer list, binding the firstnelements and silently discarding the tail.recursion_quicksort.ailprintedQuicksort: [3]instead of[1, 1, 2, 3, 4, 5, 6, 9]— rc=0, no error, no fallback warning. A direct NO-SILENT-FALLBACKS and A1 (determinism) violation, and it affected every fixed-length list pattern in every--bytecodeprogram.Root cause — one function, one operator
internal/gen/lower/match.go,lowerPatternCond,case *core.ListPattern: the empty pattern was special-cased toOpEqwhile every other list pattern gotOpGte, tail or no tail.The discriminator was already on the node.
core.ListPattern.Tail == nilis a closed[…]pattern and must beOpEq;Tail != nil(a...restpattern, or the::cons form) genuinely means at-least-n and keepsOpGte. The old empty-list case is just theTail == nil, n == 0instance of that rule, so it collapses into it.internal/vmandinternal/bytecodeare not involved — neither contains a singlePatternreference; matching is lowered away ininternal/gen.Verification (controller-run, outside the executor sandbox)
--bytecodenow prints the correct list.falling back to evaluatorlines (control:bytecode VMpresent = 1, so the grep works).MATCH 151 → 152,DIVERGE 6 → 5, delta exactlyrecursion_quicksort.ail.go build,go vet,go test ./internal/gen/... ./tests/golden/bytecode/ ./cmd/ailang,gofmt,check-file-sizes,check-boundaries,check-changelog,check-golden-drift,test-lowering,test-stdlib-ail.The pin is real, not decorative
Restoring unconditional
OpGteLANDS (sha2565142335d→fd282d4d) and BUILDS (go buildrc=0, so the red is behavioural, not a compile error), and reds both new tests with the right mechanism. The inverse arm — the same mutant with those two tests-skip'd — is rc=0 across all of./cmd/ailang. So the new tests are the sole killers; nothing else in the suite catches #505.That run also confirmed the executor's self-reported deviation: the plan prescribed
if false && p.Tail == nilas the drill mutant, which is not #505 — it makes the empty pattern match everything too, so quicksort prints[]rather than[3]. The executor spotted it and substituted the exact measured pre-fix lowering.Also in this PR
tests/golden/bytecode/, but that is a Go test package drivingpipeline→lower→compiler→vmin-process — it can never observe the CLI's stderr,--caps IOor exit code, so AC1 and AC3 were unimplementable there. The.ailfixture lives there; the tests that run it shell out fromcmd/ailang/run_bytecode_test.go.🤖 Generated with Claude Code