Skip to content

[codex] RUE-366: validate parameterized test keys#4

Closed
steveklabnik wants to merge 276 commits into
trunkfrom
codex/rue-366-param-key-validation
Closed

[codex] RUE-366: validate parameterized test keys#4
steveklabnik wants to merge 276 commits into
trunkfrom
codex/rue-366-param-key-validation

Conversation

@steveklabnik

Copy link
Copy Markdown
Owner

Summary

Fixes RUE-366 by making parameterized test expansion reject param keys that are neither known field overrides nor actually used as {key} placeholders.

The harness still supports:

  • normal template placeholders in substituted case fields such as name, source, error_contains, expected_error, stdout/stderr/stdin fields
  • per-param field overrides such as exit_code, compile_fail, preview, timeout_ms, and spec_extra
  • placeholders referenced by per-param override values, e.g. error_contains = "expected {ty}"

This closes the false-green case where a typo like exit_cod = 42 inside params = [{ ... }] was silently treated as an unused substitution variable.

Validation

  • scripts/rue fmt
  • ./buck2 test root//crates/rue-test-runner:rue-test-runner-test
  • scripts/rue test

Linear: RUE-366

steveklabnik and others added 30 commits June 11, 2026 00:51
…module (RUE-121 phase 2)

Phase 2 of the backend-dedup effort: the StructInit and ArrayInit lowering
arms — the eager flattening that populates the slot cache — were structurally
identical hand-mirrored copies in the two cfg_lower.rs files (the x86 copy
matched on TypeKind, the aarch64 copy on is_struct()/is_array(); same
semantics, drifted spelling). They now live once in agg_slots.rs as
lower_struct_init/lower_array_init.

SlotBackend grows four thin leaf methods: map_value (value_map insert),
emit_reg_move, emit_load_zero (MovRR/MovRI32 on x86, MovRR/MovImm on
aarch64), and collect_array_scalars (the backends' existing wrapper over the
already-shared types::collect_array_scalar_vregs).

Verified the refactor is behavior-preserving the same way as phase 1:
--emit asm output is byte-identical before/after on a 37-program corpus
(aggregate-heavy shapes: nested structs, fieldless structs, String fields,
multidimensional arrays, arrays of structs, struct-with-array fields,
aggregate args/returns) on BOTH x86-64 and aarch64. Full test.sh green.

Remaining phases tracked in RUE-121: consumer-side store loops (phase 3),
scheduler/liveness sharing (phase 4).

Part of RUE-121.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The chumsky error-conversion path built spans with a defaulting to_rue_span()
that dropped the FileId, so every parse error span carried FileId::DEFAULT.
In a multi-file compile the diagnostic renderer's fallback then picked an
arbitrary source file: an E0100 in b.rue could be rendered with a caret on
perfectly valid code in a.rue, with wrong line/col and a mismatched snippet.
(The E0102 path already threaded the id via parser state, which is why only
some errors misattributed.)

The defaulting helper is deleted outright so the footgun can't return.
convert_error() now takes the FileId (threaded from ChumskyParser::parse) for
both the primary span and the labelled-context spans, and the
error_recovery() Item::Error span tags the file id via map_with + parser
state.

Three CLI cases in crates/rue-cli-tests/cases/multifile_errors.toml pin the
attribution: parse error in the second file, in the first file (swapped
order), and an E0102 control — each asserting the correct bad.rue:line:col
appears and the valid file's name never does. Full test.sh green.

Fixes RUE-115

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…schedulers

Remaining items of the aarch64-emitter correctness epic (items 5 and the rest
of 4/6).

Branch fixups (item 5): apply_fixups masked branch offsets to the encoding
field width with no range check — a B beyond +-2^25 instructions or a
B.cond/CBZ beyond +-2^18 would silently branch somewhere else entirely. Both
fixup kinds now ICE on out-of-range offsets, mirroring the x86 emitter's
rel32 check, plus a debug_assert that branch sites/targets are 4-byte
aligned.

Scheduler clobbers (item 4, second half): build_dep_graph created edges INTO
a clobbering instruction (clobberer after prior readers/writers) but never
recorded the clobbered registers as written by it. A later instruction
writing or reading a clobbered register therefore had no edge back to the
clobberer and could be hoisted above it — e.g. on x86 a `mov rdx, imm`
scheduled above the CQO/IDIV that clobbers rdx. Clobbers now update
last_writer/last_readers exactly like writes, on both backends. On aarch64
every current clobberer (Bl, Svc) is also a scheduling barrier, so this is
defense-in-depth there; on x86 CQO/CDQ/IDIV clobber without being barriers.
Verified behavior-preserving on the 37-program asm corpus (byte-identical on
both targets at the same base) — the new edges only forbid reorderings that
would have been wrong.

Test gaps (item 6, partial): three scheduler unit tests pin the clobber
ordering (x86 WAW + RAW vs CQO; aarch64 WAW + RAW vs SVC at the dep-graph
level), and the two @syscall result-READING spec cases — previously
x86-only, which is exactly how the Svc result-hoist bug shipped undetected —
now have aarch64-linux mirrors (getpid = 172) that CI's native arm64 job
executes.

Remaining in the epic after this: TestEmitter drift and aarch64 emitter
fuzz coverage.

Part of RUE-129.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…fixed

Two repro-confirmed miscompiles from the never-type/loop interaction, both
producing memory-unsafe binaries from innocuous source:

- `let x: i32 = loop { break; }; x` compiled cleanly and segfaulted: the
  loop expression was typed `!` unconditionally, so binding it passed type
  checking, but the break-exit path never produced a value — the binding
  read an uninitialized slot. As a function tail expression the break-exit
  block had no ret and control fell off the end of the function. (RUE-76)
- `loop { break 42; }` parsed as `break` plus an unreachable expression
  statement, compiled with only a warning, and segfaulted the same way —
  the spec forbids a value operand on break. (RUE-56)

Sema (both implementations) and HM inference now track a per-loop break
flag (loop_break_stack): a loop containing a break that targets it is typed
`()` — Rust semantics — so both RUE-76 repros become honest E0206 type
errors; a loop with no reachable break keeps type `!` (still usable in
never position). The parser now parses an optional value operand on break
(carried through RIR) and sema rejects it with new diagnostic E0502
"'break' with a value is not supported"; inference deliberately does not
count a valued break as a loop exit so E0502 surfaces rather than a
confusing type mismatch.

Spec: 4.8:17 rewritten (no-break loop is !), 4.8:21 rewritten (loop with
break is ()), new legality rule 4.8:22 (break must not carry a value).
Traceability stays 100%.

Tests: CLI cases in crates/rue-cli-tests/cases/loop_break.toml (both
repros as compile_fail, break-as-statement and no-break controls,
break-with-value in loop and while); spec cases for 4.8:21/4.8:22.
Full test.sh green.

Fixes RUE-76
Fixes RUE-56

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…s fixed

Sema's place-tracing path (Sema::try_trace_place) emitted PlaceRead and
PlaceWrite for projections — field reads, array indexing, method receivers,
call arguments, @dbg operands — without ever inserting the root variable
into used_locals. Only the comparison path and bare VarRef loads marked
usage, so a variable accessed solely through a projection warned as unused:
a tail-position `s.a`, `let x = s.a`, `g(s.a)`, `a[0]`, `@dbg(s.a)`, and a
method call on a field receiver (`h.s.len()`) all fired the lint on a
clearly-used variable. (`s.a == 5` happened to be fine because comparisons
take a different path — which is why the gap survived.)

One marking point fixes the family: the try_trace_place wrapper now inserts
the trace's root variable into used_locals on every successful trace. This
also makes projection writes (`s.a = 7`) count as a use, matching Rust's
lint; direct assignment to the variable itself (`x = 6`) does not go
through place tracing and intentionally still warns.

Eight new UI cases in crates/rue-ui-tests/cases/warnings/unused.toml: both
RUE-135 repros plus call-argument / array-index / @dbg / field-write
no-warning cases, and two still-warns controls (genuinely unused struct
variable; direct-assignment-only variable). Full test.sh green.

Fixes RUE-135

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…se 3)

Phase 3 of the backend dedup: every site that writes an aggregate's slots —
the Alloc arm (array/struct/String branches), the Store arm's whole-aggregate
paths, and all nine store loops in lower_place_write /
lower_place_write_with_projections (frame-slot stores, inout-pointer stores,
and dynamically-addressed stores) — was a hand-mirrored loop pair across the
two cfg_lower.rs files. They now iterate via two shared primitives in
agg_slots.rs:

  store_slots(vals, base_slot)                    one frame store per slot
  store_slots_through_ptr(vals, ptr, static_off)  descending ptr - off - i*8

SlotBackend grows the two corresponding leaf methods (emit_store_slot =
MovMR/Str; emit_store_through_ptr = MovMRIndexed/StrIndexedOffset). The
Alloc String branch's three unrolled stores collapse into the same
store_slots call (the 3-slot shape is already pinned by the debug_assert on
the accessor result). Address computation for dynamic indices stays
per-backend (Lea/SubRR64 vs AddImm/SubRR — genuinely different instruction
selection).

Verified behavior-preserving the same way as phases 1-2: --emit asm is
byte-identical before/after on the 37-program aggregate corpus on BOTH
x86-64 and aarch64 at the same base. Full test.sh green.

Remaining in RUE-121: call-arg/return marshalling (phase 4), then the
scheduler/liveness tables.

Part of RUE-121.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…opping them

link_elf collected relocation records only while merging .text sections;
the .rodata/.data/.bss merge loops ignored theirs entirely. Any pointer
stored in constant or initialized data — panic-Location file/string
pointers in the runtime's .data.rel.ro tables, compiler_builtins' FUNC
tables, future vtables/const tables — linked as NULL bytes. Confirmed in
the wild: the runtime archive's objects carry R_X86_64_64 relocations in
.data.rel.ro.* and .data.* sections that today's linker drops on the
floor.

The fix threads a PatchHome (Text/Rodata/Data) through each pending
relocation:

- collection is extracted into one collect_section_relocations helper
  (it was already duplicated between the ELF and Mach-O paths' .text
  loops) and now runs for the ELF .rodata and .data merge loops too;
- the ELF apply loop selects the patch buffer and base vaddr by home, so
  PC-relative relocations in rodata/data measure from their own section's
  address, and the existing bounds checks now check against the
  originating buffer rather than merged_text (epic item 8 for ELF).

.bss is NOBITS and cannot carry patch sites; unchanged.

Two regression tests hand-build objects whose .rodata/.data carry an
Abs64 relocation at offset 0 and assert the linked segment contains the
target's virtual address (verified both FAIL with the collection calls
disabled — they pin the drop). Full test.sh green; every suite program
links through this path and runtime spot-checks behave.

Scope: ELF only. The Mach-O path still drops non-text relocations and has
its own layout items (3-7) tracked in the epic.

Part of RUE-131 (items 1 and 8, ELF).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…port-prefixed directive names

Two diagnostic-precision fixes from the diagnostics-quality epic, both in
the logos lexer:

Invalid escapes (item 6): the error span ran from the start of the string
literal to the bad escape, and the reported character came from
slice.find('\\') — the FIRST backslash in the token — so a literal with a
valid escape before the invalid one ("hello \n \q") underlined the whole
string and named \n instead of \q. LexError::InvalidStringEscape now
carries the backslash's offset within the token and the actual rejected
character; the diagnostic puts a two-character caret exactly on the
offending escape.

@import-prefixed names (item 5): "@Important" died in the lexer with
"unexpected character: @" (with a 7-character caret) because the fused
@import token matched and then failed its word-boundary callback, which
logos does not backtrack from. A sibling regex now wins the longest-match
for "@import" plus identifier chars and tokenize() splits it into At +
Ident, so such names behave like every other @-directive. The word-boundary
callback is gone — the regex makes it unreachable. (Pre-existing and
unchanged: unknown directives like @other are accepted silently; noted in
the epic.)

The stale test pinning "@importx must be a lex error" is replaced by tests
asserting the At + Ident split with correct spans, plus a span-precision
test for the escape fix. Two UI cases pin the user-visible diagnostics
(error_contains "2:23" for the caret position). Also verified item 7 of the
epic (missing operand after binary operator) is already fixed on trunk —
the error points at the offending token now. Full test.sh green.

Part of RUE-133 (items 5, 6; item 7 verified already fixed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Two move-checking holes from the soundness epic, both verified by repro
before and after:

Projected by-ref arguments (item 3): passing a struct field as a borrow or
inout argument (peek(borrow o.f)) sailed through sema and panicked codegen
("by-ref argument must be a variable", cfg_lower.rs:1798). By-ref arguments
must now be plain variables — new E0438 with a "passing a field is not yet
supported" note. Covers function calls, method calls, and array elements,
on both sema implementations. Place-address lowering is the long-term fix
and stays in the epic.

Moves out of inout parameters (item 4): a callee could move out of an
inout param (fn steal(inout o: D) -> i32 { consume(o) }) with no
reinitialize-before-exit requirement, leaving the CALLER's variable
moved-from — a use-after-free shape that returned garbage today (observed
exit 127 for a 7+7 program). Any move out of an inout param is now
rejected flatly with E0437; the diagnostic documents the deliberately
conservative rule (reinitialization tracking may relax it later).
Field-level moves out of borrow params now also get the whole-var E0429
treatment. Inout forwarding (g(inout v) inside f(inout v)) keeps working
via an explicit byref-arg flag that replaces the old mark-then-unmove
hack — and composes with the loop re-move machinery (move-out in a loop
body errors; inout args in loops still compile).

Item 6 of the epic (Copy reads through a moved ancestor) was re-verified
still broken and remains tracked — the fix needs ancestor-moved checks on
Copy reads plus FieldSet auditing, too invasive to bundle here.

Fifteen CLI cases in byref_params.toml: the E0438/E0437/E0429 rejections
(including in-loop and reinit-then-return variants and a multi-file @import
case pinning the lazy path), plus controls for whole-var borrow/inout,
forwarding, reassign-only inout, and Copy field reads. Note E0438 was
renumbered from the worker's E0436 at integration: PR rue-language#930 takes E0436 for
DuplicateFunctionDefinition. Full test.sh green.

Part of RUE-127 (items 3, 4; remaining: 5, 6).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…jectivity; drop-ordering pinned

Three more items from the test-infrastructure-trust epic:

Golden cases (item 6): a spec case combining golden-IR output with
execution assertions ran only the golden comparison — its exit_code /
expected_stdout were silently dead (reproduced: a case with correct
expected_air, WRONG exit_code, and bogus expected_stdout passed). The
harness now runs both: golden --emit comparisons first, then compile+run
when execution assertions are present; compile_fail + golden-IR is
rejected loudly as unsatisfiable. The dead "asm" header mapping is gone.
One in-suite case (cfg_multiple_variables_drop_order) now exercises the
combination live.

ErrorCode injectivity (item 7): nothing guarded against two ErrorKind
variants mapping to the same E-code. A new test parses the code() mapping
and asserts every ErrorCode constant is claimed by exactly one variant —
no duplicates, no orphans, no undeclared refs. This is timely: the two
worker branches in this very cycle independently claimed E0436, which this
test would have caught at queue-merge (renumbered to E0438 in rue-language#931).

Drop-ordering (item 8): the drop-semantics spec cases asserted only exit
codes, so they passed under fully INVERTED destruction order. They now pin
order with @dbg destructor output: reverse declaration order ("2\n1\n"),
per-branch drops, early-return drops, and declaration-order field drops
(all verified against the real compiler). linear_copy_reverse_order_error
tested nothing its name claims (no @copy in its source, compile_fail
false); it now actually asserts the grammar rejects "linear @copy" in
reverse order.

Full test.sh green.

Part of RUE-132 (items 6, 7, 8; remaining: bare compile_fail assertions,
buck command_test targets, harness timeouts).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…mber merged Path patterns; one dup-symbol helper

Three driver-layer fixes from the CLI-hardening epic:

Stray intrinsic type arguments (item 8, silent miscompile): AstGen dropped
IntrinsicArg::Type when lowering args for expression intrinsics, so
@syscall(a, (), b) silently deleted the middle argument and shifted b into
the wrong syscall register. The stray arg now lowers to a TypeConst
placeholder and sema reports E0702 (intrinsic argument type mismatch)
instead of miscompiling. CLI cases in intrinsic_args.toml.

Rir::merge Path patterns (item 9): merge renumbered every InstRef except
the module ref inside Path match-arm patterns, leaving refs from the second
file onward pointing into the wrong file's instruction space. Now
renumbered like everything else (preserving the u32::MAX None sentinel),
with unit tests for both. The corruption is latent today — sema's enum
resolution ignores the module ref — so a pinning multi-file CLI case
documents current behavior rather than a user-visible repro.

Duplicate-symbol detection (item 6): the same check was hand-written three
times (merge_symbols, the parallel RIR path, and CompilationUnit::parse)
and reported duplicate FUNCTIONS with DuplicateTypeDefinition. One
detect_duplicate_symbols helper now serves all three sites, and functions
get their own DuplicateFunctionDefinition error kind.

Full test.sh green.

Part of RUE-130 (items 6, 8, 9; remaining: 2-5, 7).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…itively

The module system resolved imports only against files already passed on the
command line — nothing in the pipeline ever read a resolved module file
from disk. So `const utils = @import("utils")` failed with E0704 unless the
user hand-listed every module, and the let-bound form resolved the path but
called into a function table that never contained the module's functions
("undefined function"). The stdlib was unusable, full stop.

The driver now runs a transitive discovery pass before compilation: each
source's token stream is scanned for the `@import("...")` shape (token
level, so comments and string contents can't confuse it), the path is
resolved against the filesystem mirroring sema's ModulePath order —
$RUE_STD_PATH/_std.rue then an adjacent std/ dir for "std"; exact path for
"foo.rue"; {path}.rue then the _{basename}.rue facade for simple paths —
relative to the importing file's directory and then the root source's, and
each newly found file is loaded as if listed on the command line.
Discovered files are scanned too (worklist), with canonicalized-path dedupe
handling diamonds and cycles. Unresolvable imports are left for sema's
existing E0704 with candidate context.

Sema's ModulePath::Std arm was a hardcoded None ("not supported yet"); it
now matches a loaded _std.rue facade, so `const std = @import("std")`
resolves end-to-end and direct members work: std.abs(-5) returns 5 via
either an adjacent std/ directory or RUE_STD_PATH.

CLI cases: known_bug markers removed from the three module shapes this
fixes (let-bound call, top-level-const call, directory facade — all now
green), plus new cases for a transitive a->b import chain and direct std
member access. The remaining gap is member access through a const module
RE-EXPORT (pub const math = @import(...) — the std.math.abs chain, E0707):
filed as RUE-136 with the two std-chain cases re-pointed at it.

Full test.sh green.

Fixes RUE-14

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…s for String and large aggregates

The by-value aggregate calling convention disagreed with itself in three
ways, each a silent miscompile (the RUE-106 epic's remaining children):

- String returns: callers reserved an sret buffer but user-defined callees
  returned in registers and never wrote it — the caller read len=0 from its
  own untouched stack. Every String-returning function was broken (RUE-92).
- Args past the register budget (6 integer slots on x86-64, 8 on aarch64):
  the caller pushed them on the stack, but the callee read every param from
  its frame param area, which the prologue populated only for
  register-passed slots — slots past the budget read zeros or ICEd
  (RUE-13, RUE-91), including multidimensional arrays and [T; 8]+ (RUE-79).
- Aggregate returns wider than the return registers silently dropped slots
  (RUE-78's struct shapes).

The convention is now explicit and documented at type_uses_sret_return and
the ARG_REGS/RET_REGS definitions:

- Arguments: flattened slots fill the register budget; the rest go on the
  caller's stack. The callee prologue allocates frame slots for ALL params
  and copies stack-passed slots down, so every body access is a uniform
  frame-slot load — the divergent over-budget branches in both cfg_lower
  files are deleted. This also fixes a latent spill-slot-below-rsp bug when
  param count exceeded the budget.
- Returns: values fitting RET_REGS are unchanged; String (always) and
  over-budget aggregates use a caller-allocated sret buffer whose pointer
  rides as a hidden first argument. The callee pins it to a frame slot and
  stores all slots through it via the shared agg_slots::store_slots_to_sret.

Both backends change together; aarch64 verified by asm inspection locally
(CI's native arm64 job executes it). All six known_bug = "RUE-13" xfails in
abi.toml now pass and are un-marked (regression tests), plus new cases for
String returns (with and without args) and a 9-slot struct with stack args.
Stress-verified: >6 scalar args, inout past the budget, 16-slot
multidimensional pass+return, chained 11-slot sret calls. Full test.sh
green.

Fixes RUE-92
Fixes RUE-13
Fixes RUE-91
Fixes RUE-79
Fixes RUE-78

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…le), panic-hook ICE banner, full benchmark metrics

The --emit pipeline diverged from normal builds in four user-visible ways
(driver-hardening epic, items 2-5 and 7):

- Module programs failed E0704 only under --emit: the emit path called the
  frontend without sema's file_id->path mapping. A new
  compile_frontend_from_ast_with_file_paths threads it through (the old
  entry point delegates with an empty map), so @import programs emit IR
  exactly like they build.
- Warnings were silently dropped in every --emit mode: the frontend state's
  warnings were computed and discarded. They now print to stderr via the
  multi-file formatter, same as a normal build.
- Multi-file --emit was impossible: with no -o, the legacy two-positional
  parse claimed the second FILE as the output path ("refusing to use
  'b.rue' as the output path") — but --emit produces no executable. Under
  --emit every positional is now a source file. One parse_args unit test
  pinned the old dead-output behavior and is updated.
- --benchmark-json source metrics measured only the first file; bytes,
  lines, and tokens now sum across all files.

Also installs a panic hook (item 5): a compiler panic now prints an
"internal compiler error: this is a bug in rue" banner with the version and
the issue-tracker URL after the standard panic output, instead of ending on
a bare backtrace pointer.

Item 2's reported lex-error misattribution under --emit tokens no longer
reproduces (file ids ride the tokens since the FileId threading fixes);
covered by the multifile_errors cases.

CLI: new emit_pipeline.toml cases pin warnings-under-emit and
modules-under-emit; the harness gains compile_stderr_contains (positive
mirror of the existing not_contains) to assert warnings on a successful
compile. Full test.sh green.

Part of RUE-130 (items 2-5, 7; remaining: item 6 landed in rue-language#930).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ily)

Sema computed move facts and discarded them before AIR; drop elaboration
dropped every slot at scope exit regardless, so a value moved into a
callee, into another local, or out via return had its destructor run TWICE
— a double-free with heap types, masked today only by the no-op allocator
(the RUE-108 epic, led by RUE-61).

Move facts now flow into AIR as a passthrough MarkMoved instruction emitted
at every whole-variable move site in both sema pipelines (builtin by-ref
receivers cancel their marker; by-ref args emit nothing). CfgBuilder tracks
moved slots path-sensitively — insert on MarkMoved; clear on
reinitialization; if/match joins intersect the non-diverged branch states;
loop and short-circuit edges restore conservatively — and skips the Drop
(never the StorageDead) for moved slots.

Callees now drop their owned by-value params at exit (new Air.param_drops,
cleared for destructors to prevent self-recursion): the caller's spurious
drop had been masking a callee-side leak, so suppression alone would have
produced zero drops.

Entirely AIR/CFG-level — zero per-backend codegen changes; both backends
verified via asm (params reload from uniform frame slots).

Verified by exact destructor output across: move-into-call, param-to-local,
local chains, move-out-via-return, by-value method receivers, reassignment
after move, array moves, borrow controls, early-return-in-branch, plus
drop-exactly-once and LIFO-order controls. Eleven CLI cases in
drop_moves.toml with exact @dbg stdout; three spec cases strengthened with
expected_stdout (spec already mandates drop-exactly-once; no text changes).

Honest residuals, pinned rather than papered over: partial FIELD moves
(RUE-62) still re-drop the moved field via whole-struct glue — pinned with
a known_bug case encoding the desired output; branch-divergent moves
(moved in one arm only) conservatively keep the exit drop until runtime
drop flags exist — pinned by unit test. Full test.sh green.

Fixes RUE-61

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…rate specialization to fixpoint

The comptime-generics epic's three roots, all repro-verified before/after:

Argument type-check (the soundness hole): the inference generator's
is_generic branch never added the equality constraint between argument and
substituted parameter type that the non-generic branch adds. Scalars
silently truncated (i64 passed where T=i32), bools coerced to ints,
literals range-checked against i32 instead of T — and the aggregate case
was memory-unsafe: passing struct A where T=B compiled and the callee read
B-sized fields from an A-sized allocation (out-of-bounds read). A new
extract_type_argument builds the substitution map (type literals, named
struct/enum refs, forwarded type params; refuses shadowing locals) and the
constraint is now added; a second check in sema's generic-call path
compares each runtime argument's AIR type against the substituted
parameter type, catching shapes inference can't see (type values held in
locals). Both report the existing E0206.

Symbol mangling: specialized names encoded Type::name(), which renders
every struct as "<struct>" — ALL struct/enum instantiations of a generic
collided on one symbol (duplicate-symbol link errors, or silently sharing
one body). Mangling now encodes type identity; two instantiations link and
get distinct bodies, verified by a layout-sensitive two-struct test.

Forwarding: passing a comptime type param to a nested generic call ICEd at
the CallGeneric arm (bare panic in the CFG builder). Specialization now
iterates to fixpoint — each round scans newly created specializations for
CallGeneric, rewrites, and queues — with dedupe via the existing map and a
64-round cap that reports a clean comptime-evaluation error instead of
looping. The CFG panic remains as a now-unreachable guard.

Frontend-only (rue-air); no backend mirroring needed. Ten CLI cases in
generics.toml cover every fixed shape. Known cosmetic residual
(pre-existing, tracked in the diagnostics epic): struct mismatches print
"expected <struct>, found <struct>" — the unifier's wording for all struct
mismatches repo-wide. Full test.sh green.

Fixes RUE-99
Fixes RUE-100
Fixes RUE-102
Fixes RUE-73
Fixes RUE-101

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ule forms are an error

Ratifies RUE-137: the directory-module facade lives INSIDE its directory
(utils/_utils.rue), matching the cited matklad article (his underscore file
is the mod.rs replacement — an earlier ADR revision misread "sorts first"
as parent-listing order) and the stdlib's existing std/_std.rue. Every
artifact that encoded the sibling layout is updated together:

- ADR-0026: design tree, rationale wording (sorts first WITHIN the
  directory; self-contained modules instead of the self-contradicting
  dual-file argument), a decision-history note, and resolution-order text.
- Driver discovery: simple imports probe {path}/_{basename}.rue.
- Sema: the in-directory facade is matched with a path-boundary check (a
  stray sibling _utils.rue no longer masquerades as a directory module),
  and get_module_identity loses its facade special case entirely — with
  the facade in-directory, "module = parent directory" is the whole rule,
  which also makes facade<->submodule private access work (it didn't,
  exposed by the intra-directory spec case).
- Spec 4.13:82 reworded; CLI and spec cases moved to the new layout, plus
  a negative case pinning that the sibling layout is not a module.

Along the way the dual resolver problem bit again: the LIVE import
resolver (analysis.rs) hand-rolled loaded-path matching and probed the
disk with its own (sibling) facade rule, separate from the structured
ModulePath the const path uses. Phase 1 now delegates to ModulePath — one
implementation — and the disk probe uses the ratified layout.

Also per review: when BOTH foo.rue and foo/_foo.rue exist, resolution no
longer silently prefers the file — it is a compile error, new E0708
"ambiguous module" (mirroring Rust's E0761), detected both among loaded
files and on disk; the driver loads both candidates so sema can report it.
The old "foo.rue takes precedence" spec case is replaced by new normative
rule 4.13:89 with a covering compile-fail test, and the stray-underscore
shape gets its own non-facade case. ErrorKind::AmbiguousModule is boxed to
respect the 64-byte ErrorKind budget.

Full test.sh green (traceability 100% including the new rule).

Fixes RUE-137

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…egen, CFG, RIR, and parser

The dead-code-as-correctness review theme (RUE-134): code that exists but
does not run, sitting next to code that runs but is wrong. Eight items,
net -1165 lines:

- x86 Lea index/scale fields: liveness counted a use, regalloc never
  rewrote, emit silently dropped — removed end-to-end (every construction
  was None/1).
- MovRMIndexed/MovMRIndexed virtual-operand emit arms contained plausible-
  looking code that pre-emission verification makes unreachable — now
  unreachable!() with the reason, mirroring aarch64's pattern.
- regalloc SplitInfo: computed for every function, never read — deleted
  (SplitReason/SplitPoint/find_loop_split_points and friends).
- BasicBlock::preds: the review said "zero consumers" but Cfg's Display and
  the destructor goldens consume it — the REAL bug was staleness after
  optimization. The stored field is gone; compute_predecessors() is now
  pure and computed on demand by Display (goldens unchanged), and the
  build-time call whose result was silently discarded is dropped.
- TestEmitter (RUE-129 item 6): the drifted duplicate of the aarch64
  mov-imm encoder is deleted; its five tests now run against the real
  encoder, expectations unchanged.
- block_parser is now a thin map over maybe_unit_block_parser; the false
  "requires final expression" doc on both is corrected.
- RIR ParamRef: kept (deleting touches ~15 rue-air match arms — separate
  change); documented, with a new test pinning that astgen never produces
  it.
- FunctionSpan/RirFunctionView: dead API with an already-broken overlap
  invariant — deleted along with its producers and API-only tests.

Full test.sh green on both passes (including the preds goldens).

Part of RUE-134 (and closes RUE-129's TestEmitter item).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…rs that consume self

Two more children of the drop-elaboration epics, building on the MarkMoved
machinery:

Partial field moves (RUE-62): moving one field into a callee (eat(o.a))
still dropped the WHOLE struct — including the moved field — at scope
exit: a double-free with heap fields. MarkMoved now carries an optional
field index, sema exports single-level field moves on both pipelines, and
the CFG builder's move state went field-granular (whole slots + per-field,
intersected at joins; a projected write restores that field's drop). A
partially-moved struct's exit drop becomes: a plain CFG call to the
struct's own destructor (no glue — the existing Call path flattens struct
args, so no new codegen op on either backend) plus per-field drops in
declaration order, skipping moved fields. Verified by exact destructor
output: 800,1,777,2 — field a drops exactly once, inside the callee.
The known_bug = "RUE-62" case now passes and is un-marked.

Destructor self-consume (RUE-139): drop fn D(self) { consume(self) }
compiled and recursed at runtime (the callee's param drop re-enters the
destructor). Both destructor entry points now scan the analyzed body for
a surviving whole-value move of self and reject with new E0442 (Rust's
E0509 spirit); cancelled by-ref markers leave no trace, so borrowing
receivers stay legal.

Honest residuals, pinned: branch-divergent moves still conservatively
double-drop on the moving path (needs drop flags); nested partial moves
(o.a.b) keep the conservative whole-slot drop; destructors moving non-Copy
FIELDS out of self are not yet rejected (documented in code). Five new CLI
cases, one spec case on the existing drop paragraphs, three CFG unit
tests. Full test.sh green.

Fixes RUE-62
Fixes RUE-139

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…arm-precise spans, E0600 names missing variants

Four citizen-quality items from the diagnostics epic, now more visible
since generics started type-checking aggregates:

- Type names (item 2): unify-error conversion and the sema error paths now
  render named types via the type pool — "expected B, found A" instead of
  "expected <struct>, found <struct>", "cannot match on type 'P'" instead
  of '<struct>', with array types rendered structurally and integer-
  literal inference vars as "{integer}". The dead-but-correct
  type_mismatch_error helper is finally wired (the dead-code theme again).
- Direction (item 1): the general expected/found swap was fixed earlier;
  the one remaining reversed arm was Array-vs-non-array in the unifier —
  fn main() -> i32 { [1,2,3] } now reports "expected i32, found
  [{integer}; 3]" instead of the reverse.
- Arm spans (item 3): a match arm whose result conflicts with the other
  arms was reported against the WHOLE FUNCTION because an integer-literal
  arm's unbound type var silently absorbed the conflicting arm; the
  unifier now tracks int-literal vars through var chains and rejects
  non-integer binds at the offending constraint — the error points at the
  arm. One existing UI case had pinned the old whole-scrutinee span as a
  documented regression; it now asserts the section's stated intent.
- E0600 (item 4): non-exhaustive match errors name what's missing via one
  shared helper used by BOTH match analyzers ("missing variants: Green,
  Blue"; "pattern `false` is not covered"; wildcard suggestion for
  integers).

Thirteen new UI cases across type_names, type_mismatch_direction, and
match_diagnostics; full-string assertions where stable. Known limitation
recorded in the epic: fn f() -> [i32; 3] { 5 } still reports E0800 rather
than a mismatch (though it now names the array type). Full test.sh green.

Part of RUE-133 (items 1-4; remaining: 8-10 and the pre-existing silent
unknown-directive acceptance).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
… works

The ADR-0026 re-export idiom — pub const math = @import(...) in a facade —
was invisible to module member lookup: member access consulted functions,
structs, and enums in the module's file, but never consts, so
std.math.abs(-5) failed with E0707 "module 'std' has no member 'math'"
while std.abs worked (RUE-136, the last gap from the RUE-14 split).

Member access on a module type now also consults consts declared in the
module's file (matched via the declaration span; visibility enforced like
other members): a module-typed const yields its module, so chains resolve
member-by-member. Value consts (pub const PI = ...) still fall through to
the unknown-member error — const evaluation through the member path is a
separate feature.

Architecturally this is one edit in one place: member ACCESS is the
single-copy path shared by both sema pipelines, and once the access types
the receiver as a module, both member-CALL paths handle the chain
unchanged. (The call pair is still duplicated — that dedup lands
separately, after the in-flight worker branches that touch it.)

The two known_bug = "RUE-136" CLI cases (std via adjacent dir and via
RUE_STD_PATH, both using the std.math.abs chain) now pass and are
un-marked; two new spec cases pin the re-export chain and that a non-pub
re-export stays private. Full test.sh green.

Fixes RUE-136

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…pipeline cure)

analyze_module_member_call_ctx (lazy pipeline) and
analyze_module_member_call_impl (eager pipeline) were ~100-line hand-mirrored
copies — and had already drifted: the eager copy skipped the exclusive-access
check entirely.

The shared body — visibility, arity, and argument-mode checking, plus the
call-args encoding and Call emission — now lives once as
check_module_member_call / emit_module_member_call. Each pipeline's wrapper
shrinks to its genuinely pipeline-specific seams: function lookup (same map,
two access paths), argument analysis (which recurses into the owning
pipeline), and the exclusive-access check. The eager wrapper now runs its
exclusive-access check (the drift is corrected in the direction of checking
more; the path is only reachable through module values, which require
imports and thus normally take the lazy pipeline — so no user-visible
behavior changes, confirmed by the full suite).

Two pre-existing holes found while extracting, verified by repro and filed
rather than silently changed:

- RUE-140: member calls resolve ANY global pub function, never checking the
  function belongs to the receiver module's file — utils.sneaky() happily
  calls other.rue's sneaky(). The shared body is now the single fix site.
- RUE-141: the two pipelines' exclusive-access checks have drifted (the lazy
  one misses borrow duplicates and lvalue checks) — left as a per-pipeline
  seam with comments pointing at the issue.

Behavior-preserving otherwise; full test.sh green.

Part of the dual-pipeline unification (RUE-134 family; relates RUE-120).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
… weak-symbol semantics

Two more items from the linker-correctness epic:

Arch validation (item 10): ObjectFile parsed the ELF machine type and then
discarded it — the linker would happily combine objects of different
architectures, confirmed in the review as an "aarch64" output containing
22 x86 syscalls. ObjectFile now records its machine (the Mach-O parser
only accepts ARM64 and tags accordingly), and add_object refuses objects
whose architecture doesn't match the link target with a new ArchMismatch
error. Strengthens the RUE-36 cross-compilation story: a wrong-arch
runtime archive now fails loudly at link instead of producing a binary
that dies on the first instruction.

Weak symbols (item 9): two fixes to match standard ELF semantics. Among
multiple WEAK definitions the FIRST now wins (the old code replaced an
existing weak with any later definition, weak included); a strong
definition still overrides a weak one in either order, and two strongs
still collide. And a relocation against an UNDEFINED weak symbol now
resolves to address 0 instead of an undefined-symbol error — the standard
optional-symbol idiom.

Three new unit tests: arch mismatch rejected; the weak ordering matrix
(weak/weak keeps first, weak/strong overrides, strong/weak keeps strong);
and an end-to-end link asserting the patched imm64 for an undefined weak
is 0. Full test.sh green.

Part of RUE-131 (items 9, 10; remaining: 2-8, 11 — the flat symbol map and
the Mach-O layout family).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…reject malformed relocations

Three Mach-O items from the linker epic, plus a fourth bug found while
fixing them:

__TEXT sizing (item 3): the segment's file size was hardcoded to exactly
one 16KB page. Any program whose headers + code exceeded that had its code
spill past the declared segment — the data segment then overlapped it in
the file and dyld mapped garbage. The segment now grows to the page
boundary after the code ends, and __DATA starts where __TEXT actually
ends.

Layout drift (found during item 3): the relocation pre-pass
(calculate_text_file_offset_for_dynamic) and build_dynamic computed the
layout INDEPENDENTLY and had drifted — the pre-pass counted one data
section where the builder counts up to three (__const/__data/__bss), so
with more than one data section every relocation was patched against a
text offset the binary didn't use. Both now consume one
compute_dynamic_layout, and a test pins their equality with all three
sections present. (The dual-implementation disease, linker edition.)

bss addresses (item 4): link_macho computed bss symbol vaddrs as
data_vaddr + aligned data length + bss_offset_in_data — but
bss_offset_in_data IS the data length, so every bss symbol pointed one
data-length past its storage. The double-count is removed.

Malformed relocations (item 6): a non-extern relocation with
r_symbolnum == 0 underflowed (r_symbolnum - 1) and indexed out of bounds —
a parser panic on malformed input. Now a clean ParseError.

The first push of this change FAILED macOS CI with garbage string
output — its own medicine: the unified computation ran before
build_dynamic pushed __LINKEDIT, while the pre-pass builder had it
pre-added, so the two disagreed by one segment header and every
relocation was patched for an offset the binary didn't use. The shared
computation now counts __LINKEDIT conditionally (present-or-added), and a
regression test mirrors the linker's exact asymmetric usage: pre-pass
builder with manual __LINKEDIT versus build builder without, asserting
the offsets agree.

Three new layout tests (the >1-page growth invariant, the
precompute/build equality with all data sections, and the asymmetric
pre-pass/build agreement) plus the suite. Mach-O execution is validated
by CI's macos job; layout invariants are pinned at the unit level here.

Part of RUE-131 (items 3, 4, 6 + the layout drift; remaining: 2, 5, 7, 8
Mach-O-side, 11).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ss check for both pipelines

Two soundness fixes on top of the member-call dedup:

Membership (RUE-140): member calls looked the function up in the GLOBAL
table and checked only visibility — utils.sneaky() resolved to other.rue's
pub fn and returned 99. The shared call body now takes the receiver
module's file (threaded from the receiver's module id in both pipeline
wrappers) and rejects callees defined elsewhere with E0707, the same
module-scoped error member access uses; unknown members also report E0707
instead of a generic undefined-function. New spec legality rule 4.13:90
with a covering case, plus four CLI cases (the rejection, a same-module
control, the facade re-export chain, and facade/inner non-membership).

Exclusive access (RUE-141): the eager pipeline's check (inout duplicates,
borrow/inout conflicts, lvalue-ness) and the lazy pipeline's (inout
duplicates only) had drifted. Honesty note from verification: the issue's
claimed regular-call repro was FALSE — regular calls in the lazy pipeline
route through the eager methods, and the drifted _ctx check was reachable
only via the dead parallel pipeline. The drift WAS user-visible through
module member calls on trunk: m.swap(inout x, inout x) and
m.observe(borrow x, inout x) both compiled (verified against the old
binary). There is now ONE implementation with the eager semantics
(check_exclusive_access_in); both former checks are thin adapters, the
drifted _ctx variable extractor is deleted, and four CLI cases pin the
previously-accepted shapes as errors.

Full test.sh green; traceability 100%.

Fixes RUE-140
Fixes RUE-141

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ough moved ancestors

The last two children of the move-checking epic:

Linear may-move (item 5): must-consume read the same union-at-joins state
that use-after-move uses, so a linear value consumed in only ONE branch
passed the check — and match arms never saved/restored move state at all.
Move state now tracks consumed-on-all-paths separately (intersected at
joins; one-sided branches merge against the pre-branch state; match arms
analyze from the pre-match state and join properly in both pipelines;
diverging branches count as consuming). New E0443 "linear value is not
consumed on all paths" with a consumed-here label. Side benefit: consuming
a value in two DIFFERENT match arms is no longer a false use-after-move.

Copy through moved ancestor (item 6): let a = consume(o.f); let b =
o.f.x; was accepted — reading a Copy field through a moved ancestor, a
use-after-free now that drops are move-aware. The Copy-field branch of
field access checks every ancestor prefix in both pipelines, rejecting
with the existing E0205. The reassignment control exposed a pre-existing
hole — field ASSIGNMENT never cleared moved state — fixed alongside
(index-projection places excluded to stay sound).

Both compose with the loop back-edge recheck and the field-granular drop
state, pinned by in-loop variants. New spec rules 3.8:50-56 with eleven
covering cases (traceability 100%), fourteen CLI cases, four unit tests.
Full test.sh green.

Fixes RUE-127

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ented system

The entire module system's normative spec was ten paragraphs inside the
@import intrinsic's section. This adds chapter 10 (docs/spec/src/10-modules/,
five sections plus index) covering what is implemented and verified today —
every rule and example was run against the compiler before being pinned:

- Module forms: file modules and directory modules with the in-directory
  facade (foo/_foo.rue, per the RUE-137 ratification), the sibling form's
  non-meaning, and the E0708 dual-form ambiguity (referencing 4.13:89).
- Import resolution: full candidate order including std via $RUE_STD_PATH
  then adjacent std/, importer-relative before root-relative, transitive
  disk loading, and load-once cycle dedupe (cycles are legal).
- Visibility: pub is cross-directory; non-pub items are visible within
  their directory, facade included; re-export chains through pub const,
  and non-pub re-exports staying private.
- Module bindings: const (top-level) and let (local) binding forms and
  their equivalence; modules are not runtime values (argument, field, and
  comparison positions error).
- Program composition: the flat-namespace duplicate-symbol rule, pinned
  with an informative note marking it transitional.

The existing 4.13:79-90 intrinsic rules are referenced, not moved —
consolidation is a separate change. Sixteen new spec cases across three
new case files plus annotations on ~20 existing module cases; normative
traceability stays 100% (539/539, up from 513). Two incoherent behaviors
were deliberately left unpinned and documented in the issue: member-call
results in arithmetic positions, and module expressions in unit positions.

Fixes RUE-138

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…on every path

The last structural piece of the drop-elaboration epic. A value moved on
only SOME paths (one if/match arm, a break-guarded loop move, a
short-circuit edge) is not "moved on all paths", so the static analysis
must keep its scope-exit drop — which double-dropped on the moving path.
This was the documented, test-pinned residual of the move-suppression
work.

The CFG builder now maintains runtime drop flags, Rust-style: a pre-scan
finds every slot with a whole-value move marker anywhere in the function;
each such droppable slot gets a hidden i32 frame slot (via the existing
temp-local allocator, so both backends see the grown frame through
cfg.num_locals with zero codegen changes). The flag is armed (1) at the
value's initialization and every reinitialization — Alloc, Store,
ParamStore, whole-place writes, and function entry for owned params — and
cleared (0) at each move marker. Scope-exit and param drops for flagged
slots are emitted behind an `if flag != 0` diamond.

Because the flag tracks the EXECUTED path, this corrects every shape the
static intersection is conservative about, not just if/match divergence:
the loop-with-break move and short-circuit edges also drop exactly once
now. Statically-decided cases keep their existing zero-cost handling
(moved-on-all-paths drops stay elided outright; never-moved slots get no
flag and no guard).

The unit test that pinned the double-drop as a documented residual now
asserts the guarded shape instead. Five CLI cases pin exact destructor
output across both paths of if and match divergence, the loop-break move,
re-arming on reassignment after a divergent move, and owned-param
divergence. Remaining epic residual (noted, unchanged): nested partial
moves (o.a.b) keep the conservative whole-slot behavior.

Full test.sh green twice.

Fixes RUE-108

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…trict

Two quick wins from a buck2 setup review:

.buckconfig gains [project] ignore = .claude, .jj. Agent worktrees under
.claude/worktrees each carry a full repo copy WITH its own buck-out, all
inside the buck2 project root: their vendored prelude BUCK files broke
every `//...` target pattern outright (uquery choked parsing a worktree's
bundled prelude), and their constant churn streamed file-change events
into the root daemon, invalidating its in-memory graph — the likely root
of the recurring "0% cache hits" rebuilds. .jj's op store writes on every
command in this colocated repo and is cheap insurance. Verified: `//...`
patterns resolve again with the ignore in place.

find_rue_binary loses its guessing fallback. When RUE_BINARY was unset it
globbed every buck-out config-hash directory and picked the NEWEST
compiler binary by mtime — with debug, release, and historical configs
side by side that silently selected the wrong compiler (a real incident
this week: a just-edited suite ran against a mid-rebuild binary and
reported false failures). Resolution is now: RUE_BINARY, then the bin/rue
symlink scripts/rue build maintains, then a loud error telling you to set
one — never a guess.

Full test.sh green (it always sets RUE_BINARY, so behavior there is
unchanged; the strictness only bites formerly-silently-wrong ad-hoc runs).

The remaining review findings (BUCK macro dedup, toolchain-level
deny_lints, rue-runtime's nine never-compiled test modules, the inert CI
buck cache, sh_test promotion, stale-config GC) are tracked in Linear.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Plans the cure for the codebase's most expensive structural problem:
semantic analysis exists as three parallel implementations (eager Sema
methods, the lazy _ctx family, and a dead #[allow(dead_code)] parallel
pipeline), and the live two have drifted repeatedly — five confirmed
instances are catalogued, including "same program, different verdict
depending on whether it has an import" (RUE-141).

Proposes option B: delete the dead parallel pipeline, run a systematic
differential audit of every _ctx/method twin (differences become bug
fixes with pinning tests, landed separately), re-point the sequential
lazy driver at the Sema methods cluster by cluster deleting _ctx
functions as their callers disappear, and collapse SemaContext to what
the lazy scheduler actually needs. Each phase independently shippable
and suite-verified; diagnostics changes forbidden during the mechanical
phase. The two completed dedup cuts (rue-language#943 member calls, rue-language#946 exclusive
access) are the proven pattern this scales up.

Doc only — no code changes.

Part of RUE-134.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
steveklabnik and others added 28 commits July 4, 2026 01:40
Pure, behavior-preserving refactor. analysis.rs was one 7736-line `impl Sema`
(3.5x the next-largest file), where most future language work happens. Split
the impl into 9 focused submodules under sema/analysis/ (anon_methods,
builtin_ops, calls, functions, instructions, intrinsics, ownership, pointers,
type_inference), each a verbatim `impl Sema` block; the root drops to 1510 lines
(free fns + enum + shared helpers + mod decls). Largest new module is
intrinsics.rs at 1312 lines.

Only non-move edits are mechanical: private -> pub(super) on moved methods for
cross-submodule resolution, super:: -> super::super:: for sema-relative paths in
moved bodies, and widening the consistency-test include_str! scan to the whole
analysis/ tree. No logic, error-code, or behavior change — function bodies moved
verbatim.

clippy clean; test.sh Pass 24, 100% normative traceability (697/697), 342
rue-air unit tests pass. (The source-scanning consistency tests failed right
after the split, then passed once the include set widened — confirming the
safety net actually exercises the moved arms.)

Fixes RUE-4

Co-Authored-By: Claude <noreply@anthropic.com>
…g enums (RUE-348 miscompile)

From the rue-cfg code review. A real SILENT MISCOMPILE at -O1+ (the default opt
level): fold_enum_comparison folded an enum ==/!= using only the variant index
(get_enum_variant dropped payload_len), so two operands of the same variant with
different payloads folded to "equal". Sema allows ==/!= on any enum structurally
(RUE-285) and codegen compares tag+payload via emit_aggregate_equality, so -O0
and -O1 disagreed:

  enum Opt { None, Some(i32) }
  if Opt::Some(5) == Opt::Some(6) { 1 } else { 0 }   // -O0: 0 (correct), -O1: 1 (WRONG)

Fix: get_enum_variant now returns payload_len; fold_enum_comparison only folds
when BOTH operands are payload-less (payload_len == 0). Fieldless-enum folding
(the @target_arch() == Arch::X86_64 case) stays; payload-carrying comparisons
fall through to codegen's structural equality. Verified: Some(5)==Some(6) now
exits 0 at -O0/-O1/-O2; Some(5)==Some(5) exits 1. Added a differential_opt
regression case (compiled + run across all opt levels, asserts agreement).

Also fixed a misleading comment in fold_shift: a shift amount >= the bit width is
masked mod the width (spec 4.3a:10), not UB — the fold still defers the masked
case to the backend (which masks correctly), but the comment no longer
contradicts the spec + dce.rs.

clippy clean; test.sh Pass 24, 100% traceability, oracle-diff 0.

Fixes RUE-348

Co-Authored-By: Claude <noreply@anthropic.com>
…atements prose pass

- RUE-344: the arena allocator sized fresh arenas as align_up(header_size +
  min_size, 4096), ignoring that alloc_from_arena places the block at
  align_up(header_size, align) — for align > 8 that padding was uncounted and the
  fast path has no bounds check, so alloc(65512, 16) returned a block 8 bytes past
  the mmap region. Threaded `align` into alloc_arena (size includes the aligned
  offset + full size); fixed the false SAFETY comment. 2 boundary unit tests
  (align 1..4096). Not codegen-reachable today (only align 1/8 emitted); the
  surface is the public __rue_alloc ABI.
- RUE-323 (ADR-0043 Phase 2): the bulk Vec→ArrayBuf rename already landed
  (62de487); cleaned up the 5 stragglers it missed (spec 9.2:9 + 4 CLI case
  comments). Verified ArrayBuf(i32) runs; Vec(i32) cleanly errors E0202.
- RUE-299: filled the remaining documenting-existing-behavior spec gaps (source
  encoding 2.0:5/6, never-propagation 3.4:6a, let shadowing/wildcard 5.1:14/16,
  items taxonomy 6.0:1a, field/method name spaces 6.4:33, const-eval trapping
  6.5:12/14, @dbg format 4.13:8a), each verified by execution; 16 spec cases. The
  two design-gated remainders (duplicate-param-name policy, reserved-type-name
  table) are split out / tracked elsewhere.
- RUE-202 (statements chapter): retired folk-terms in ch5 against the formal core
  — value-denotation for statements (type (), no value), assignment as a store
  into a place with overwrite-drop, RHS-first eval — cited to core §5.2/§6.7/§6.8.
  No normative change; verified by execution.

clippy clean; test.sh Pass 24, 100% normative traceability (705/705), oracle 0.

Fixes RUE-344
Fixes RUE-323
Fixes RUE-299

Co-Authored-By: Claude <noreply@anthropic.com>
… struct (RUE-322)

The ADR-0043 Phase-1 slice runtime — the keystone that blocks the entire
str/StrBuf chain (RUE-324..327). Three prior workers judged the 2-word fat-pointer
ABI a multi-session codegen effort; the winning insight is that a second-class
slice is *just* a 2-field @copy struct {ptr: @raw, len: u64} desugared onto
existing intrinsics — so all three documented ABI blockers collapse into ZERO new
codegen and ZERO new MIR variants, working on both backends for free.

Implemented (all in rue-air, under --preview slices):
- `[T]` becomes a synthetic 2-field struct {ptr, len}; slice params pass by-value
  through the existing multi-slot struct ABI.
- array→slice coercion (HM inference relaxed); `borrow arr` (a [T;N]) desugars to
  {@raw(arr[0]), N}.
- `s.len()` → the len field read; `s[i]` → `@assert(i < len)` + `@ptr_read(@ptr_offset(ptr, i))`
  (runtime bounds-checked, trapping); slice-param forwarding.
- retired the E0486 not-yet-implemented gate for the slice-param position;
  un-marked both slices_mvp xfails; fixed the stale E0486 CLI/UI cases.

Verified by execution: sum(borrow a) over [i32;3] = 60; OOB s[i] traps (exit 101);
s[1]=5, .len()=6, [u8] stride correct, forwarding=60, element-type mismatch →
clean E0206. aarch64 via --emit asm: correct bounds-check + ptr sequence, no ICE.

clippy clean; test.sh Pass 24, 100% traceability (705/705), oracle-diff 300 seeds
0 disagreements. Deferred (follow-ups): inout write-slices, range slicing x[a..b],
ArrayBuf/Vec slices.

Part of RUE-322

Co-Authored-By: Claude <noreply@anthropic.com>
…l & lazy-analysis ADRs

- RUE-132 (harness robustness/correctness, Part of): spec expected_stdout now
  compares byte-exact (strips only the TOML """ boundary newline, not per-line
  trailing whitespace / internal blanks) so a stdout regression can't pass the
  spec suite while failing the CLI runner; differential_opt cases missing an
  explicit stdout/exit_code are rejected at load; a case with error_contains but
  no compile_fail (silently never checked) now panics at load; run_with_timeout
  drains stdout+stderr on reader threads (no >64KB deadlock, RUE-338 class); the
  golden-IR --emit path runs under the timeout. 13 new tests.
- RUE-333: form-feed (U+000C) is no longer skipped as whitespace, resolving the
  lexer-vs-spec drift (2.3:1 lists only space/tab/nl/cr). Research confirmed the
  interesting split Steve flagged: Rust treats FF as whitespace (verified by
  running rustc), Zig does not — matched spec + Zig (strict). A form-feed source
  now errors cleanly (E0001) instead of silently compiling.
- RUE-245 (Part of): ADR-0044 optimization-levels — researched GCC/Clang/rustc/
  Zig level conventions; proposes Rue's -O0/-O1/-O2/-O3 contract centered on the
  observable-behavior-identical-across-levels invariant (enforced by the RUE-236
  differential harness), with -O2/-O3 honestly aliasing -O1 today. Docs only.
- RUE-328 (Part of): ADR-0045 lazy-semantic-analysis — documents the decided
  Zig-style compile-on-reference model (conditional-compilation-for-free, the
  unreferenced-decl tradeoff, incremental module-loading rollout). Docs only;
  implementation to be broken into tasks next.

clippy clean; test.sh Pass 25, 100% normative traceability (705/705), oracle 0.

Fixes RUE-333

Co-Authored-By: Claude <noreply@anthropic.com>
… pass

- RUE-181 (Part of): ADR-0046 "Delete Flat Multi-File Mode" (Accepted) —
  designs the warn→error migration to require @import for all cross-file refs,
  recommends explicit-@import (no auto-import, don't block on prelude/rue.toml),
  main.rue as an ordinary module, and measures the blast radius (~36 flat-mode
  CLI cases / ~18 spec cases). Docs only; implementation tasks follow.
- RUE-19 (Part of, items 2-4): use-after-move E0205 now ends with a
  copy-pasteable `help: pass it by borrow instead: \`borrow x\`` (all 8 sites);
  `impl Block {}` gets "Rue has no impl blocks; define methods inside the struct
  body"; fixed the occurs-check <struct> name leak. Item 3 (duplicate inout-self
  error) refuted — already deduped.
- RUE-202 (Part of, expressions chapter): retired folk-terms in the operator
  sections (4.2 arithmetic, 4.3 comparison, 4.3a bitwise, 4.4 logical) against the
  formal core (§5.4/§5.8/§6.4), verified by execution (overflow/div-zero/MIN÷-1
  trap; shift-modulo-width; structural == leaves operands usable). No normative
  change.

clippy clean; test.sh Pass 25, 100% normative traceability (705/705), oracle 0.

Co-Authored-By: Claude <noreply@anthropic.com>
… warn on --log-format without --log-level

From the rue-compiler code review. Three driver UX fixes in the rue CLI:
- --help listed only 7 of the 11 supported --emit stages (omitting lowering,
  liveness, regalloc, stackframe) — a user hitting a typo saw all 11 via the
  error path, but --help disagreed. Now derives the list from
  EmitStage::all_names() (the single source of truth).
- --emit combined with --benchmark-json corrupted stdout (IR text + benchmark
  JSON on the same stream, making it unparseable). The normal compile path
  already suppresses its 'Compiled ...' line under --benchmark-json for exactly
  this reason; the emit path never did. Now rejected early with a clear error.
- --log-format without --log-level was a silent no-op (logging off by default, so
  the format never applied). Now warns.

(-j ignored in --emit mode → RUE-352; the .rue-only clobber guard → RUE-351 —
both filed rather than rushed.)

clippy clean; test.sh Pass 25.

Co-Authored-By: Claude <noreply@anthropic.com>
- RUE-324 (ADR-0043 Phase 3): `str` type CORE — a {ptr,len} view reusing the
  slice representation (gated --preview string_trio), static-backed first-class
  string literals in .rodata, `.len()` (byte length), bounds-checked byte-index
  `s[i]` (traps 101 on OOB), and first-class use as param/return/field/reassign.
  Key correction: slice `s[i]` strides by slot_count*8 (right for 8-byte-slotted
  array elements, WRONG for packed rodata bytes) — str indexing routes through a
  new packed-byte runtime `__rue_str_byte_at`. Both backends verified. Deferred
  (clean errors, no soundness gap): str concat/compare/interop/char-iter, range
  slicing, second-class borrow str.
- RUE-132 (compile_fail honesty, completes the epic): a load-time guard now
  requires every compile_fail case to assert its error (both harnesses), and the
  106 bare spec cases (28%) were backfilled with their real E-code. Found +
  fixed a deeper bug: expand_case silently dropped per-param error assertions, so
  the inout_exclusive_access "same variable" check had never run. Traceability is
  now honest — skipped/preview-allowed-to-fail tests no longer count as coverage,
  exposing 5 normative rules covered only by skipped @allow-directive tests (a
  gated self-clearing allowlist; 99.3% real). Marked the str cases
  preview_should_pass so implemented-but-preview-gated rules count correctly.
- RUE-202 (types chapter): retired folk-terms in 3.4 never/coercion (transfers
  control away; (Sub-Never) subsumption) and 3.9 destructors (drop glue; drop-once
  = moved-out-skip → no double-free), cited to the core.

clippy clean; test.sh Pass 25, honest traceability 99.3% (705/710, 5 allowlisted),
oracle-diff 0.

Fixes RUE-324
Fixes RUE-132

Co-Authored-By: Claude <noreply@anthropic.com>
…e) form

The E0102 message for a Rust-style `as` cast suggested '@intcast(T, value)' — a
two-argument form that does not exist: @intcast takes ONE argument (the value;
the target type comes from context / the binding), so following the hint gave a
second error (E0701 'expects 1 argument, found 2'). Spec 9.2 confirms the 1-arg
form (@intcast(got)); @cast is not a real intrinsic (E0700). Corrected the hint
to '@intcast(value)', which now compiles + runs.

Found while capturing agent-ergonomics data (RUE-353): the coding agent kept
reaching for `as` (huge training-corpus prior), and the error's own suggested
fix then misled it a second time. The UI test pins the '@intcast' substring, so
it stays green.

clippy clean; test.sh Pass 25.

Part of RUE-353

Co-Authored-By: Claude <noreply@anthropic.com>
…rose

- RUE-349: duplicate parameter names were silently accepted (first-wins). New
  E0491 DuplicateParameter at sema (check_duplicate_param_names in the FnDecl
  arm — covers free fns, methods, assoc fns, anon-struct methods), pointing at
  the second occurrence (added a span to RirParam). `self` + distinct params is
  fine. Spec rule 6.1:34 + 4 spec / 5 UI / 4 CLI cases.
- RUE-352: -j/--jobs was ignored in --emit mode. Hoisted the rayon-pool config
  to configure_thread_pool(jobs), called once in main() before the emit/compile
  dispatch, and removed it from compile_multi_file_with_options (build_global
  panics on a second call). --emit -j1/-j2 now honored.
- RUE-351: the output-clobber guard only matched a .rue suffix. Now compares
  resolved paths (clobber_key), so `rue prog -o prog` / `rue ./prog -o prog` /
  `rue prog -o sub/../prog` all error with the source intact; `rue a.rue -o out`
  still compiles.
- RUE-202 (chapter 8 runtime behavior): retired folk-terms ("can fail",
  "detected", "cause a runtime panic") into the core's exact trap vocabulary
  (the four trap categories, (Panic-Lift), fixed exit 101), cited to §6.4/6.5/6.12.

clippy clean; test.sh Pass 25, honest traceability 99.3% (706/711, 5 allowlisted).

Fixes RUE-349
Fixes RUE-351
Fixes RUE-352

Co-Authored-By: Claude <noreply@anthropic.com>
From the rue-builtins code review (runtime-ABI + definition consistency):
- test_all_string_methods_present omitted `contains` and `starts_with`, so
  dropping either declared method (both have runtime backing) would have left
  the runtime symbol orphaned with the guard test still green. Added both.
- The "how to add a builtin" Vec worked-example called __rue_free(ptr, size) —
  the real runtime signature is __rue_free(ptr, size, align) (3 args). Fixed the
  exemplar so a future StrBuf/ArrayBuf destructor implementer targets the right
  ABI.
- The freestanding-ops block claimed to be the "single source of truth" for
  directly-lowered runtime symbols but omitted String/str byte-indexing and
  chars(); narrowed the comment and pointed at them.

(Two needs-decision findings filed to Backlog: RUE-354 reserved-name guard misses
the runtime's memcpy/memset/_main; RUE-355 bool-helper return-register cleanliness.)

clippy clean; test.sh Pass 25.

Co-Authored-By: Claude <noreply@anthropic.com>
- RUE-326 (ADR-0043 Phase 5): the `Str(N)` fixed string rung. Reuses `str`'s
  2-word {ptr,len} representation via a new is_str_like unifier, so NO codegen
  changes on either backend. Supports: literal-fits construction (an over-long
  literal is a clean compile error, E0492 — renumbered from the worker's E0491,
  which RUE-349 had taken for DuplicateParameter), `.len()` (current byte length
  ≤ N), bounds-checked byte-index `s[i]`, and coercion/borrow to a `str` view.
  New rules 3.7:49-55; 11 spec + 8 CLI cases (preview_should_pass). Verified by
  execution: Str(8)="hello" -> len 5, s[0]=104; Str(3)="hello" -> E0492;
  borrow-to-str, multibyte, OOB trap, round-trips all pass.
  Model note: followed the CAPACITY model (Str(N) holds up to N bytes, len ≤ N),
  the string analog of [u8; N]. Deferred (noted): mutation/append, and genuine
  inline-[u8;N] storage (construction is static-backed rodata today —
  observationally identical for the immutable literal-only surface, since Rue
  arrays are 8-byte-slotted and a packed inline buffer is a larger codegen task).
- RUE-202 (items chapter 6): retired folk-terms in constants (6.5 — comptime-set
  membership 4.14:26-29), enums (6.3 — compile-time error, E0421/E0420), and
  structs (6.2 — mutable field as an assignable place, 5.2). No normative change.

clippy clean; test.sh Pass 25, honest traceability 99.3% (713/718, 5 allowlisted),
oracle 0.

Fixes RUE-326
Part of RUE-321

Co-Authored-By: Claude <noreply@anthropic.com>
…ated alias (RUE-325 increment)

ADR-0043 Phase 4 green increment: StrBuf is now the canonical built-in name
(display, dispatch, sret check on both backends, all diagnostics); String
remains a silent deprecated source-level alias resolving to the same synthetic
struct, so all ~249 existing String sites keep compiling. Runtime symbols stay
__rue_String_* (internal). Fixed two alias-induced invariants: pool/registry
by-name aliasing, and all_struct_ids de-dup (a drop-glue double-generation
hazard). Integration: renumbered the informative alias rule to 3.7:57 (the
worker's 3.7:49 collided with Str(N) rules 3.7:49-55 that landed in rue-language#1162
after its base).

Verified by execution on the merged tree: StrBuf new/push_str/len/println/
@to_string/concat (exact stdout), String-alias parity exit 0, struct StrBuf
E0404, aarch64 --emit asm sret path, and a cross-mechanism Str(N) x StrBuf
program (5 / 104 / hello / exit 0). ./test.sh exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ass)

Precision-only rewrite of docs/spec/src/07-arrays/01-fixed-arrays.md: array
literals get value-denotation + ownership (cites (Array-Intro) §5.8, (D-Array)
§6.5); indexing denotes the element place, value context yields copy/move
(cites (D-Index) §6.5); bounds rules state the exact check point and the trap
(i negative or i >= n, exit 101; cites (D-Index-Trap) §6.5, §6.12, ch 8.2).
No requirement changes; rule IDs and categories stable. Avoided 7.1:43-48
(RUE-299) and the 7.1:25-31 move-legality prose.

Verified by execution: arr[1] -> exit 42; runtime OOB -> "error: index out of
bounds", exit 101. ./test.sh exit 0, traceability baseline unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ity, stale comment

Four verified findings from the rue-rir component review (2026-07-04):

- gen_for anchors the @__rue_iter_len intrinsic (and its collection VarRef) on
  the ITERABLE's span, so the not-iterable E0206 underlines the offending
  expression instead of the whole for statement. UI case pins 3:14.
- The --emit rir dump no longer hides load-bearing fields: Alloc prints
  [iter_elem], InfiniteLoop prints "borrows <coll>", and FnDecl/ConstDecl/Alloc
  print their directives like StructDecl already did (shared format_directives
  helper, StructDecl deduplicated onto it).
- @copy with arguments is now a compile error ("@copy takes no arguments",
  spec 2.5) instead of silently accepted-and-ignored; @Allow args unaffected.
  UI case added.
- PARAM_SIZE doc comment corrected to the real 7-word layout (was stale at 4,
  missing is_comptime + the 3 span words).

Verified by execution: E0206 caret at 3:14 under notiter; @copy(garbage)
errors; bare @copy + @Allow(unused_variable) still work (exit 10, no
warnings); rir dump shows [iter_elem]/borrows xs. clippy clean, ./test.sh
exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…o-case guard, arity guards

Four verified findings from the rue-oracle component review (2026-07-04):

- ZST parameter slots (real oracle bug): the compiler gives zero-sized types
  ZERO ABI slots (abi_slot_count) but still emits a Param read for them at the
  NEXT param's index; the oracle forced every arg to >=1 slot, so any fn with
  a ZST param before a value param made the oracle read the wrong slot and
  report a phantom DISAGREE blaming codegen. Fixed with a type-aware Param arm
  (is_zero_sized/zero_sized_value over type_pool) + zero-width arg layout;
  regression tests incl. two-level forwarding. The naive fix (drop .max(1))
  was insufficient — the CFG shares the ZST's index with the next param,
  found via --emit cfg during integration.
- Spec-mode stdout compared with normalize_golden, which trims trailing
  whitespace/boundary blank lines — a whitespace-shaped oracle-vs-compiler
  divergence scored AGREE. Now byte-exact modulo the single """ boundary
  newline via rue_test_runner::strip_block_boundary_newlines (made pub),
  matching the spec runner and corpus mode.
- finish_report returned SUCCESS with zero cases checked (existing-but-empty
  dir, TOMLs with no parseable cases) — a corpus-wiring regression would turn
  CI green while testing nothing. total==0 is now FAILURE.
- string_builtin arity + type guards: a runtime-fn signature drift (the
  RUE-314 class) now yields an honest Unsupported skip instead of reading args
  positionally from wrong slots / coercing non-strings to "". Also fixed the
  inverted over-shift comment in the fuzz generator (shifts mask, never trap).

Verified by execution: ZST probe DISAGREE->agree; ws-divergence spec case
AGREE->DISAGREE; empty/no-case dirs exit 0->1; corpus and spec tallies
byte-identical to baseline (498/78/518 and 1259/94/571, 0 disagreements) so
the guards introduced zero false skips. clippy clean, ./test.sh exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…lently ignoring/false-clobbering

Two verified findings from the driver component review (2026-07-04):

- Broken pipe: Rust startup ignores SIGPIPE, so `rue ... | head` made every
  stdout write panic and the RUE-130 panic hook printed the full "internal
  compiler error, please report this" banner (exit 134) for a benign
  condition. main() now restores SIG_DFL on Unix: the process exits silently
  with SIGPIPE (141) like every other CLI in a pipeline.
- --emit mode never writes the output path, but -o was both silently ignored
  (no file, no message — `--emit asm a.rue -o out.s` leaves the user waiting
  for out.s) and unconditionally clobber-guarded (`--emit ast a.rue -o a.rue`
  falsely refused). The guard now applies only to executable-producing mode,
  and an explicit -o under --emit warns that IR goes to stdout. Unit test
  pins both sides.

Verified by execution: pipe to head -0 -> status 141, no banner; --emit with
-o -> warning, exit 0, no file created; --emit -o <source> accepted; plain
-o <source> still refused; normal compile unaffected. clippy clean,
./test.sh exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…+ CLI compile timeout + shared ice_message

Six verified findings from the test-harness component review (2026-07-04) —
the "next RUE-132" sweep. Four are gate-integrity holes where a case that
runs nowhere (or is allowed to fail) still counted as spec coverage:

- Duplicate rule ids: parse_spec_file was last-writer-wins, so an informative
  restatement authored after a normative rule silently ERASED the coverage
  requirement (the near-miss 3.7:49 incident). Duplicates are now collected,
  printed, and fail the gate.
- Spaced Zola shortcode args: `cat = "normative"` (spaces) silently demoted a
  rule to informative and `id = "..."` made it invisible — Zola renders both
  fine. The hand-rolled arg extraction is now whitespace-tolerant.
- only_on platform typos: `only_on = ["x86-64-linx"]` skipped the case on
  EVERY host while its spec refs still counted as coverage. Platform names
  are now validated against KNOWN_TARGETS at load time, like preview names.
- Param-level preview overrides: traceability read preview flags from the
  BASE case only, while the runner honors per-param overrides — a param row
  adding `preview` was allowed to fail yet counted as coverage. The coverage
  computation now mirrors expand_case.

Plus: the CLI harness compile step now runs under the per-case timeout
(mirroring the spec runner — a comptime hang wedged the whole suite), and
ice_message is a single shared pub implementation (the CLI copy deleted).

Each hole verified by execution before AND after with scratch
RUE_SPEC_DIR/RUE_SPEC_CASES corpora: dup-id gate 0->1, spaced-args uncovered
ids now reported, only_on typo now a loud load failure, param-preview case
now uncovered. The REAL corpus passes the strengthened gate (holes were
latent, nothing was being masked). clippy clean, ./test.sh exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…all args), goto_join helper, dead API

Five verified quality findings from the rue-cfg component review (2026-07-04;
behavioral lanes — joins, divergence, opt soundness, drops — came back CLEAN):

- --emit cfg now prints EnumVariant payload OPERANDS (enum_variant #3::0(v0,
  v1)) instead of an opaque count, matching StructInit/ArrayInit; a tuple
  variant's dataflow inputs were unreadable.
- Codegen tracing (format_cfg_inst_data) now takes the Cfg: PlaceRead/
  PlaceWrite render with projections RESOLVED ($0.#2.1) via a new pub
  Cfg::place_to_string instead of raw arena index ranges ($0[3..5]); and the
  Call/Intrinsic/StructInit arms print their actual args now that the "no Cfg
  access" constraint their comments cited is gone.
- New CfgBuilder::goto_join single-sources the value-branch/join-param arity
  contract that was open-coded identically at THREE sites (if-then, if-else,
  match-arm) — the exact seam the RUE-347 bug class lives on; the copies
  could previously drift between constructs.
- Deleted dead pub Place::is_simple (zero callers workspace-wide).

Verified by execution: enum_variant dump shows operands; if/match join values
correct at -O0 and -O2 (exit 42); enum payload round-trip exit 7. clippy
clean, ./test.sh exit 0 (incl. golden corpora — the goto_join refactor is
behavior-identical).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…n alloc failure, _start RSP contract

Verified findings from the rue-runtime component review (2026-07-04) — the
first sweep of this crate since the heap/StrBuf workstream landed.

- MEMORY SAFETY (heap.rs): a near-usize::MAX @alloc wrapped the arena page
  round-up (unchecked align_up(total, 4096)) to zero, clamped up to the 64 KiB
  default arena, and handed out a NON-NULL pointer claiming a ~2^64-byte block
  backed by a 64 KiB mapping — a later write SIGSEGVs / corrupts memory. Now a
  checked round-up returns null, honoring the documented OOM contract. Reachable
  from a checked{} block via a 1-byte element type (bypasses RUE-345's upstream
  count*size multiply). Distinct from RUE-344 (alignment padding, already fixed).
- MEMORY SAFETY (string.rs): __rue_String_push / push_str wrote through
  new_ptr.add(len) without checking new_ptr for null, so a failed
  string_ensure_capacity became a wild write through null+len. Both now
  republish the original string unchanged on allocation failure, matching the
  null-guard every sibling allocator already uses (all 3 arch copies).
- heap.rs align > page size: blocks are placed at an offset aligned relative to
  the page-aligned arena base, so align > 4096 could return a misaligned
  address despite the doc guarantee. No reachable caller exceeds 8; over-page
  aligns are now rejected cleanly (null) rather than silently misaligned.
- entry.rs x86-64 _start: the asm block did `sub rsp, 8` without restoring it
  and without options(noreturn), then fell through to platform::exit — an
  inline-asm stack-pointer contract violation (UB) that worked by luck. Added
  the matching `add rsp, 8`. The aarch64 entry points never had this.

Verified by execution: the overflow @alloc now returns null (exit 0) instead
of SIGSEGV; normal @alloc (42) and 4000-byte String growth (160) intact.
New heap.rs unit tests pin the overflow and the over-page-align rejection; new
CLI case pins the overflow end-to-end. clippy clean, ./test.sh exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…+ cmp->tst Cbz escape + sub dedup (aarch64)

Latent-gap and drift fixes from the rue-codegen emit/peephole/schedule
component review (2026-07-04). The behavioral core came back SOUND — the
x86 peephole and scheduler were verified miscompile-free by -O0-vs-O3
differential + objdump byte-checks (no RUE-146 regression) — so these are
defensive hardening, not live-bug fixes:

- x86 emit_setcc / emit_movzx (byte operands): emitted REX only for r8-r15,
  so a byte r/m with encoding 4-7 (SPL/BPL/SIL/DIL) would decode as the
  legacy high-byte AH/CH/DH/BH — a silent wrong-register access. Now force a
  bare REX for encoding >= 4. Latent today (no reachable caller passes those
  regs); unit tests pin `sete sil` = 40 0F 94 C6 and `movzx eax, dil` =
  40 0F B6 C7. Byte output for all current programs is unchanged.
- aarch64 emit_cmp_imm: silently masked the immediate to 12 bits (a bare
  imm & 0xFFF) with no guard, unlike the RUE-45/129-hardened ADD/SUB
  encoders. Now uses the plain or LSL#12 12-bit form and panics on an
  unencodable immediate instead of truncating. Identical bytes for every
  current caller (all < 4096).
- aarch64 cmp->tst peephole: Cbz/Cbnz (register-tested conditional branches)
  fell through the escape-boundary check, so the cmp flags could be treated
  dead across a taken conditional edge. Added them as escape boundaries —
  strictly conservative (can only suppress a rewrite), matching x86.
- aarch64 emit_sub64_rr was byte-identical to emit_sub_rr; deleted the
  duplicate and pointed its one caller at emit_sub_rr (drift risk removal).

Verified: x86 program correct at -O0 and -O2; aarch64 --emit asm unchanged
for current programs (fixes are behavior-preserving by construction); codegen
unit tests (incl. aarch64 byte-pinning) + ./test.sh green; clippy clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…et span

Two verified findings from the rue-parser component review (2026-07-04):

- ICE (robustness): the RUE-42 nesting-depth guard (check_nesting_depth)
  omitted the postfix `?` (try) operator from its counted-token set, so a long
  `x????...?` chain built AST depth unbounded by the 256 cap and overflowed the
  parser stack (exit 134, no diagnostic). `?` wraps in exactly one AST node
  like `.`, so it is now counted the same way; a long chain rejects cleanly
  with E0482. UI regression case added.
- span: an index on the LHS of an assignment (AssignSuffix::Index) built its
  place span from index.span().end — stopping before the closing `]` — while
  the RHS index path already captures the bracket end. Threaded the `]` offset
  through AssignSuffix::Index so `a[i] = ...` and the intermediate `a[i]` in
  `a[i].f = ...` span the whole `base[index]`, matching the RHS.

Verified by execution: 2000-`?` chain now E0482 (exit 1) not stack overflow;
index assignment runs (a[1]=40 -> 41); parser unit tests + ./test.sh green;
clippy clean. Larger findings (empty-body if/while/for misparse, exponential
parse on unterminated braces / nested array literals, sub-parser duplication)
filed separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Four small fixes from the rue-lexer/rue-span/rue-target review sweep:

- Bare CR (U+000D) inside a string literal is now an unterminated-string
  error, matching bare LF, the backslash-before-EOL path, and spec 2.3:1
  (CR is a line terminator) / 2.1:9. Previously a lone CR was silently
  swallowed as string content.
- A leading UTF-8 BOM still errors (it is not in the spec whitespace
  set), but the E0001 diagnostic now carries a help explaining that the
  invisible character is a byte-order mark, instead of pointing a caret
  at nothing visible.
- Spec 2.4:2/2.4:3 keyword tables now list pub, const, checked,
  unchecked, ptr, and Self — all reserved by the lexer and backing
  shipped features (modules, checked blocks, pointer types, Self), but
  absent from the normative tables. Spec tests pin each as rejected in
  identifier position. (Inverse direction of RUE-331, which stays open
  for impl/type.)
- TokenKind doc said Ident/String carry a `Symbol`; the type is Spur.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…Es, purge dead surface

Fixes from the rue-error component review:

- UnexpectedCharacter/InvalidStringEscape messages render the offending
  character through escape_debug, so a NUL/VT/BOM/control byte in source
  shows as a visible \u{..} escape instead of corrupting the one-line
  diagnostic with a raw control byte.
- The ice!/ice_error! macros now attach the compiler version
  automatically (the doc claimed callers should - none ever did), so
  graceful ICE reports carry the same version line as the panic-hook
  banner. VERSION is single-sourced in rue-error; the CLI's --version
  reads it through rue-compiler instead of a duplicate const.
- Deleted never-constructed surface: ErrorKind::UnexpectedEof/E0101
  (the parser reports EOF via UnexpectedToken; number retired, not
  reused), CompileError::at, and IceContext::with_target + its target
  field (dead builder; per-ICE state belongs in details).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ifications

From the rue-fuzz component review. Three lenses independently converged
on the timeout hole; the CI hole was verified against two real nightly
runs that found a crash nobody was told about.

- harness: every input now runs under a wall-clock deadline (default 5s,
  --per-input-timeout). The pipe read and waitpid are both bounded; at
  the deadline the child is SIGKILLed and reported as a new
  RunOutcome::Timeout with its own dedup signature. Previously a hung
  compile (e.g. the RUE-374 superlinear parse) blocked waitpid forever:
  the entire hang/DoS bug class was undetectable AND one such input
  wedged the whole nightly run.
- targets: sema/compiler targets panic on ErrorKind::InternalError in
  the returned errors — a graceful ice_error! is an ordinary Err, so it
  previously classified as a clean run and ICEs were invisible to
  fuzzing.
- reproducers get a .meta sibling (target/signature/description) so CI
  artifacts are self-describing; mutation seed defaults to per-run
  entropy and is printed for replay (a fixed seed made every nightly
  re-explore an identical sequence).
- fuzz.yml: add the missing issues:write permission — the notify step
  has been failing with 'Resource not accessible by integration' while
  real crashes were found (2026-07-03/04). Steps are continue-on-error
  with a failing aggregator so one crashing target no longer skips all
  later targets; new crashes comment on the open fuzz-crash issue
  instead of being silently suppressed; job/step timeout-minutes cap a
  wedged run.
- purge: dead regalloc-oriented generators (arb_x86_mir_with_vregs and
  its private ladder) and the stale Regalloc-target claims; --help
  target list is now driven from all_targets().
- cache-probe.yml: pipefail so a failed measured build fails the step.

The crash the broken notification ate is filed as RUE-381.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The rubric (docs/process/spec-prose-rubric.md) converts the remaining
chapter rewrites into checklist work: ID permanence, the procedure,
register rules, category discipline, the banned folk-terms table, and
escalation triggers, with the template PRs named.

Template chapters, applying it:
- 4.5 blocks: EBNF normative->syntax; 4.5:3 split (new 4.5:7 =
  no-final-expression case); 4.5:6 'destroyed' -> dropped newest-first,
  cited to 3.9 and core (D-EndScope); core citations added.
- 4.6 if: 4.6:4 multi-claim split (new 4.6:10 = type of the if), never-
  coercion cross-ref added; 4.6:7 -> dynamic-semantics with (D-If-T)/
  (D-If-F) citations; new 4.6:11 = no-else evaluates to (); new
  informative 4.6:12 pointing at the 3.8 branch-join rules.
- 4.9 return: 'the implicit ()' retired (4.9:4 names the omitted-form
  rule); 'compatible with' -> identical up to the never coercion;
  4.9:5 rationale split to informative 4.9:11; 4.9:7 now states the
  return-path drops (3.9:18, core (D-Return)).
- 4.10 call: EBNF fixed to include inout/borrow call_arg modes
  (matching appendix A; new informative 4.10:10 for the place
  requirement, 6.1:17); 'compatible' -> identical up to never; new
  informative 4.10:11 cross-referencing argument-passing ownership;
  denotation rule 4.10:9 cited to (D-Call)/(D-Return-Value).

Coverage: new gated IDs wired to existing cases (block_empty_is_unit ->
4.5:7; if type case -> 4.6:10; execute_then_branch -> 4.6:11).
Traceability green (99.3%, only the 5 known-uncovered). Process docs'
stale r[X.Y:Z#category] marker syntax corrected to the live shortcode.

Part of RUE-202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@steveklabnik

Copy link
Copy Markdown
Owner Author

Closing: this was created against the fork by mistake. Recreating against rue-language/rue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant