Found by accident, and worse than the bug that found it. Five deliberately
broken decode cases came back green through `build.lua --test --isa x86`, while
running the same suite directly reported "253/258 PASSED, 5 FAILED".
Two independent holes, both open, so neither backstopped the other:
- the x86 test binary ended at `print_summary()` and never called `os.exit`,
so it exited 0 no matter how many cases failed;
- `do_test` looked for the words "N failed" in the output, and the x86
harness prints "N FAILED" — a lowercase-only Lua pattern, so the largest
suite in the tree was exempt from its own gate.
The exit code is the check that should have been load-bearing, so the harness
sets it. The output match stays as a backstop for a suite that forgets to, and
is now case-insensitive.
How long this has been true is not knowable from here — every x86 regression
since the pattern was written would have been reported as PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Riok9vMpkLmo78wsVKJHhz
`bswap` is `0F C8+rd` — the register rides in the opcode's low three bits, like
`push`/`pop`/`xchg`/`mov`, but behind an escape byte. The decoder's retry at the
+r base opcode was gated on `esc == .NONE`, so only the register-0 forms
(`0F C8` = bswap eax, `48 0F C8` = bswap rax) landed on a table entry directly;
the other seven registers came back INVALID_OPCODE. Emission was always correct
— every `bswap` this compiler has ever produced runs — but a disassembly
containing one stopped dead, which is how it surfaced: three functions in
sigil's JIT corpus disassembled to a header with no instructions under it.
The table was never the problem. BSWAP is in `tablegen/encoding_table.odin` and
in the generated decode table, exactly where it should be.
Fixing the gate meant looking at why the retry existed twice. There is a correct
+r handler further down, carrying comments that describe two bugs already found
and fixed in it — "scan ALL entries, not just the first", "scan for the sized
register rather than assuming op0" — and the copy inside the retry had received
neither, plus a third of its own. So the retry now re-runs only the LOOKUP and
falls through to the single handler, which fixes two more instruction families
that were equally undecodable:
- `xchg rAX, r` (90+rd): 0x90's run sorts NOP ahead of XCHG, and the copy
tested only the first entry for a +r form, so every `xchg rAX, r` was
rejected.
- `push bx` / `pop bx` / `mov cx, imm` (any +r under 0x66): the copy passed
`prefix` where the legacy row wants 0, since for legacy opcodes 0x66 is
operand size rather than part of the opcode's identity.
Two supporting changes. The shared handler's default answer is now the first +r
entry rather than `idx.start`, so a fall-through at 0x90 cannot answer NOP for
an `xchg`. And a base-opcode retry that does not land on a +r form is now an
error rather than a fall-through: decoding 0x0E as the 0x08 entry (OR) would be
a wrong instruction reported confidently, which is worse than an unknown byte.
Verified three ways. Every opcode in every escape map was decoded before and
after: 154 combinations changed, every one of them INVALID -> valid, and nothing
that already decoded decodes differently. All 24 BSWAP encodings and all 96
legacy +r encodings were diffed against llvm-mc's disassembly and agree
operand-for-operand. Six new decode-only cases pin the three families; five of
them fail without this change.
(Two remaining differences from llvm-mc are deliberate and documented here:
implicit accumulators are left unmaterialized so a short form re-encodes to the
short form, and `48 90` reads as `xchg rax` rather than `nop`.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Riok9vMpkLmo78wsVKJHhz
`base/runtime`: do not free the original block on a failed heap resize
The default heap allocator freed old_ptr when the underlying allocation
failed (allocated_mem == nil). On the realloc path (heap_resize) the original
block is left intact on failure, and on the copy/fresh path old_ptr has not
been copied or freed yet, so freeing it left the caller holding a dangling
pointer. A [dynamic] array whose resize failed therefore double-freed its
data on the next delete (reported as free(): invalid pointer / use-after-free).
Return .Out_Of_Memory without freeing anything; the caller retains ownership
of the original block.
Fixes#7262