The bounds-no-checks cases emitted and built but never ran, so they only
proved that --no-checks produces compilable C -- not that it removes the
check, which is the entire point of the flag.
A run case could not simply be appended. BOUNDS.FE returns the out-of-bounds
element directly, so with checks removed its exit code is whatever sits past
the array on the stack and there is no correct status to assert. Asserting on
the generated C instead does not work either: emit_c.c defines fe_trap_bounds
unconditionally and --no-checks only suppresses the call sites.
Add NOCHK.FE, which reads one element past a [2]i32 and returns x - x. That
is 0 for whatever garbage the unchecked read produced, so the same source has
a defined outcome both ways: compiled with checks it must trap, compiled with
--no-checks it must run to completion and exit 0. Register both halves and
drop the two BOUNDS-N cases they supersede.
Verified in DOSBox-X: 155 passed, including m3-nochk-trap failing as expected
and m3-nochk-off-run succeeding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
The highest supported milestone was spelled out in six places across four
files: range(1, 7) and default="m6" in test_cli.py, the same pair in
test_milestones_dosboxx.py, and through=6 in both registry.py and suite.py.
Registering M7 meant finding all six, and missing one failed silently.
Derive MAX_MILESTONE and MILESTONES from CASES instead, and move the mN
selector parser to registry.milestone_number so the pytest module stops
carrying its own copy. Adding cases for a new milestone is now enough for
ferro-test to accept --through/--only for it.
No behaviour change: MAX_MILESTONE evaluates to 6, ferro-test still advertises
{m1..m6} with default m6, and the case snapshot is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
registry.py was literally `cat registry_m1_m3.py registry_m4_m6.py`, which
left a duplicate `_c` definition and a stray module-level docstring at the
seam. Replace the hand-rolled append loops with five builders -- _emit, _wcl,
_triple, _rejects, _dump_ast -- and express the cases as one ordered list.
The irregularities are now parameters instead of one-off code, each with the
reason recorded:
- _triple(stem=) for M2 castwhil emitting CAST16
- _triple(build_source=) for M4 prop, which emits PROP.C but compiles
PROPTEST.C because that file #includes it
- _triple(run_suffix="trap", run_ok=False) for the M3 bounds cases
- _triple(emit_suffix=None) for M4, whose ids lack the -emit suffix
- _emit(output_first=True) for M6, which passes -o before the input
The three scattered 8.3 output-name mappings collapse into one _OUT83 table
keyed by (milestone, name). The key needs both: bad-type is BAD-TY in M2 but
BAD-TYP in M4, and bad-cond is BAD-CO in M2 but unshortened in M5. Values are
carried over verbatim -- the shortenings are inconsistent and several were
never required, but that is a separate decision.
suite.py keeps only Case and drops the all_cases forwarder, so the
registry -> suite -> registry cycle is gone along with the function-scoped
import that worked around it. dosboxx.py still imports Case from suite and is
untouched.
Verified behaviour-preserving by snapshotting (id, milestone, command,
expect_success) for all 150 cases in order before and after: diff is empty.
pytest collects the same 151 items.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW