Skip to content

feat(cpu): Status.RE, privilege-aware addressing, and the COP1 residuals that close Phase 1 - #33

Merged
doublegate merged 22 commits into
mainfrom
feat/addressing-and-privilege
Jul 21, 2026
Merged

feat(cpu): Status.RE, privilege-aware addressing, and the COP1 residuals that close Phase 1#33
doublegate merged 22 commits into
mainfrom
feat/addressing-and-privilege

Conversation

@doublegate

@doublegate doublegate commented Jul 21, 2026

Copy link
Copy Markdown
Owner

This PR closes Phase 1's cut criterion: n64-systemtest's CPU/COP0/TLB/COP1 categories are at Failed: 0.

Third and last of the stack (#31#32#33), now rebased onto main. 19 commits; the whole session took those categories from 40 → 0.

Addressing and privilege

  • Status.RE — reverse endian in User mode. A permutation of byte lanes within the doubleword, expressed as an XOR of the low address bits. Instruction fetch is swapped too. The swap is applied to the physical address, which touches only bits 2:0 and so keeps BadVAddr raw on a fault.
  • The segment map depends on (address, mode, width), never the address alone. KSEG0 does not exist in User mode, and it is the address-space check — not the TLB — that stops a user program reaching it. An out-of-range address raises AdEL before the TLB is consulted, carried by a separate TranslateError variant: folding it into a refill would send the program to the refill handler, where a well-behaved kernel maps the page and grants the access it was never allowed to make.
  • Sign extension is not optional. 0x0000_0000_8000_1000 is not shorthand for KSEG0; it is an address error. The old code truncated to u32 and accepted it — and the reset vector, the ROM entry point, and every kernel address in the test suite were stored that way, all passing on addresses the hardware rejects.
  • The 64-bit operations are Reserved in 32-bit User/Supervisor mode, from the manual's own epsilon legend (UM Fig. 16-1) rather than the per-instruction notes. Reading the legend is what caught LWU, which n64-systemtest does not exercise.
  • XKPHYS — eight 2^32 direct windows; only C == 2 is uncached.

COP1

  • FR = 0 maps fs and ft by different rules. The manual declines to define odd registers here, so the ROM's table is the oracle. Two rows admit no single mapping: SQRT.S $13, $31 reads FGR30, ADD.S $2, $28, $31 reads FGR31. Ledger C-21.
  • Integer→float honours FCSR.RM. The old converters were as casts plus a round-trip inexact check — so the flag was right and the value ignored the mode. All four were deleted rather than left unused. Ledger C-24.
  • A to-integer conversion refuses an integer source format (CVT.W.W and friends are not instructions) with Unimplemented Operation, rather than reinterpreting the register and returning a plausible number.
  • Tininess is detected before rounding. FLT_MIN / (1+1ulp) toward +∞ rounds back to a normal number and still underflows — here the value was right and the flag missing.
  • BC1F/BC1T/BC1FL/BC1TL — they were not implemented at all; the branch retired as a no-op.
  • An in-flight C.cond.fmt is forwarded to BC1. A stall cannot do this: stall_for freezes every stage, so holding the branch delays the compare's WB equally. The load interlock is not a counter-example — it works because its consumer reads through the bypass network. Ledger C-25.

TLB

  • A PageMask pair stores only its higher bit.
  • A TLB tag is masked by PageMask, not divided by the page size — division only clears low bits, which is right for all six legal page sizes and wrong once a canonicalised mask has a hole.
  • Random is a plain 6-bit down-counter reloading on == Wired; PRId.Rev is 0x22; BadVAddr reports the address the instruction named.

Verification

Every commit was green on cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, RUSTDOCFLAGS="-D warnings" cargo doc, the thumbv7em-none-eabihf no_std build, and pre-commit run markdownlint. Re-verified after the rebase onto main, and the rebased tree is byte-identical to the branch those gates ran on.

Guards are mutation-checked where the check is meaningful — the PageMask canonicalisation, the TLB masking, the underflow flag, the 64-bit reservation, and the BC1 bypass each turn a specific test red when reverted.

Not included, deliberately

DMFC0/DMTC0, which the epsilon table also marks: they are additionally governed by coprocessor usability, and in User mode COP0 is unusable so hardware reports CpU. The ROM exercises neither, so the omission is recorded as a decision rather than resolved on no evidence.

v0.2.0 is unblocked by this but not tagged — that is the VERSION-PLAN phase-close ceremony and belongs on main after this lands.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 21, 2026 03:01
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5c76a626-5a86-4940-9318-9c666e08c61c

📥 Commits

Reviewing files that changed from the base of the PR and between 6bdbbaa and a67142d.

📒 Files selected for processing (6)
  • AGENTS.md
  • CHANGELOG.md
  • crates/rustyn64-test-harness/tests/systemtest.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/cpu.md
📝 Walkthrough

Walkthrough

The PR adds privilege- and width-aware segment translation, reverse-endian memory and instruction access, COP1 condition forwarding, floating-point and TLB corrections, cache-state validation, 64-bit operation reservation checks, and sign-extended address handling.

Changes

MIPS Phase 1 semantics

Layer / File(s) Summary
Privilege-aware segment translation
crates/rustyn64-cpu/src/addr.rs, docs/cpu.md
Segment classification incorporates privilege mode, address width, and ERL; invalid addresses produce address errors before TLB resolution, with widened mappings and XKPHYS cacheability covered by matrix tests.
Pipeline translation and reverse-endian access
crates/rustyn64-cpu/src/pipeline.rs, crates/rustyn64-cpu/src/lib.rs, crates/rustyn64-test-harness/...
Data and instruction paths use the new access model, apply User-mode reverse-endian swapping after translation, and use sign-extended reset, ROM, PC, and memory addresses.
COP1 branches and operation reservation
crates/rustyn64-cpu/src/decode.rs, crates/rustyn64-cpu/src/exec.rs, crates/rustyn64-cpu/src/pipeline.rs
BC1 variants are decoded and executed from forwarded FCSR.C; MIPS III 64-bit operations are reserved in 32-bit User and Supervisor modes.
Floating-point and machine-state corrections
crates/rustyn64-cpu/src/fpr.rs, crates/rustyn64-cpu/src/fpu.rs, crates/rustyn64-cpu/src/softfloat.rs, crates/rustyn64-cpu/src/tlb.rs, crates/rustyn64-cpu/src/cache.rs, crates/rustyn64-cpu/src/cop0.rs
FR-aware register access, mode-aware integer conversion, tininess-before-rounding, TLB tag canonicalisation, cache-specific state validation, and COP0 reset/counter behaviour are updated with tests.
Phase 1 records and documentation
CHANGELOG.md, AGENTS.md, docs/STATUS.md, docs/accuracy-ledger.md, docs/cpu.md
Phase 1 status, accuracy-ledger entries, project conventions, changelog notes, and CPU behaviour documentation are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CPU
  participant ic_stage
  participant translate_via
  participant TLB
  participant Memory
  CPU->>ic_stage: request instruction fetch
  ic_stage->>translate_via: classify and translate virtual address
  translate_via->>TLB: resolve mapped segment
  TLB-->>translate_via: return physical address
  translate_via->>Memory: apply User-mode byte-lane swap and fetch
  Memory-->>ic_stage: return instruction bytes
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is on-topic, but it violates Conventional Commits: it is 91 characters and exceeds the 72-character limit. Shorten it to a Conventional Commits form under 72 characters, e.g. feat(cpu): add RE addressing and COP1 fixes.
Docs-As-Spec Sync ⚠️ Warning softfloat.rs changes visible FPU underflow/tininess semantics, but docs/cpu.md has no underflow/tininess entry; the PR body doesn’t justify omitting the CPU spec update. Add a docs/cpu.md FPU note for tininess-before-rounding/underflow, or say in the PR body why this observable flag change needs no subsystem-doc update.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly about the Phase 1 CPU/COP0/TLB/COP1 changes, so it is clearly related.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Oracle Number Is Stated ✅ Passed docs/STATUS.md states MET: Failed: 0, and the changelog records a Phase 1's categories: 1 → 0 effect, so the oracle number is stated.
Changelog Entry For User-Visible Changes ✅ Passed PASS: CHANGELOG.md has a populated [Unreleased] section covering the user-visible BC1, Status.RE, segment-map, sign-extension, TLB, and COP1 fixes.
Measured, Never Tuned ✅ Passed PASS: the only new literal is a doc comment, and it cites D-6, which records the cache-model choice; no new hardware constant/timing value was added.
Unsafe Stays Out Of The Chip Crates ✅ Passed No Rust unsafe syntax was found; all chip/core crate roots still have #![forbid(unsafe_code)], and the frontend has no unsafe block needing a SAFETY note.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances the VR4300 address-translation and privilege model to better match hardware behavior and n64-systemtest’s Privilege: / RE expectations, by making segment classification depend on privilege + addressing width, enforcing compatibility (sign-extension) rules in 32-bit addressing, and implementing Status.RE reverse-endian behavior in User mode (including instruction fetch).

Changes:

  • Implement Status.RE reverse-endian swapping in User mode only, applied post-translation on the physical address (including instruction fetch).
  • Make the segment map privilege- and width-aware (User/Supervisor/Kernel differences, 64-bit segment widening, XKPHYS), and distinguish address errors from TLB faults at the translation boundary.
  • Enforce 32-bit compatibility (sign-extension) addressing and reserve MIPS III 64-bit ops in 32-bit non-kernel modes; update harness/tests/docs accordingly.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/STATUS.md Updates Phase 1 failure counts to reflect improved n64-systemtest results.
docs/cpu.md Documents privilege/width-dependent segment mapping, sign-extension rule, Status.RE, and reserved 64-bit op behavior.
docs/accuracy-ledger.md Adds residual entry R-1 with correlated capture findings for an outstanding half-mode FP issue.
CHANGELOG.md Records the newly modeled behaviors and their impact on Phase 1 category counts.
crates/rustyn64-cpu/src/addr.rs Introduces privilege+width-aware segment classification, XKPHYS support, compatibility checks, and TranslateError to separate address errors from TLB faults.
crates/rustyn64-cpu/src/pipeline.rs Wires new translation API into pipeline, adds Status.RE handling for data and fetch, and raises address errors pre-TLB for invalid segments; adds 64-bit reserved-encoding check.
crates/rustyn64-cpu/src/decode.rs Adds Op::is_64_bit() classification for gating reserved 64-bit operations.
crates/rustyn64-cpu/src/lib.rs Fixes reset PC to be a sign-extended 64-bit address and updates corresponding tests/comments.
crates/rustyn64-test-harness/src/rom.rs Sign-extends ROM entry point to a 64-bit address to avoid invalid zero-extended kernel addresses.
crates/rustyn64-test-harness/tests/first_rom.rs Updates expected entry/subroutine ranges and direct-load calls to use sign-extended kernel addresses.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rustyn64-cpu/src/pipeline.rs Outdated
Comment thread crates/rustyn64-cpu/src/pipeline.rs Outdated
@doublegate

Copy link
Copy Markdown
Owner Author

Added one more commit, 37a9b5d, since the stack is the only place it can land.

fix(cop1): under FR=0, fs and ft resolve differently — a floating-point arithmetic instruction ignores the low bit of fs and does not ignore the low bit of ft; fd is used as-is in both modes.

The manual declines to specify this (an odd register with FR = 0 is "undefined", UM §7.5.3/§16), so it is measured against the ROM and recorded as ledger C-21 rather than cited as documentation. Two rows settle it and no single mapping satisfies both — SQRT.S $13, $31 yields sqrt(16) so fs = 31 read FGR30, while ADD.S $2, $28, $31 yields -10 + -16 so ft = 31 read FGR31. The suite then says it outright in its own assertion messages.

It does not revise C-14, which governs MTC1/LWC1 and the doubleword coprocessor moves — those really do reach an odd register's high half. Separate accessors keep the classes apart.

Subject-wise this belongs with #31, not here; it sits on top only because it depends on the stack.

Phase 1's categories: 23 → 19.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 12-69: Run the pinned markdownlint-cli v0.49.1 hook with
pre-commit across all files and resolve every reported Markdown issue in
CHANGELOG.md (lines 12-69), docs/STATUS.md (line 181), and
docs/accuracy-ledger.md (line 178), preserving the documented content.

In `@crates/rustyn64-cpu/src/decode.rs`:
- Around line 448-477: Update the operation classification match containing
`Self::Ld` and related 64-bit instructions to include `Self::Lwu`, so `LWU` is
treated as a 64-bit operation and raises RI in narrow non-Kernel modes. Extend
the reservation test for this classification with an `LWU` encoding whose result
differs between narrow and wide User mode.

In `@docs/STATUS.md`:
- Line 181: Use docs/STATUS.md as the authoritative Phase 1 source: update the
status row near line 181 to match the surrounding statement about whether the
ROM executed and the current count of 23 failures. In CHANGELOG.md lines 27 and
49, label the 24→23 and 27→24 figures as historical checkpoints or revise them
into one consistent cumulative progression; at line 68, remove the presentation
of 31→27 as the final result and align it with the authoritative current count
of 23.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8c4b7107-64d7-4d54-b666-5ae2f5d6c49b

📥 Commits

Reviewing files that changed from the base of the PR and between 84ee3eb and c3c9403.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • crates/rustyn64-cpu/src/addr.rs
  • crates/rustyn64-cpu/src/decode.rs
  • crates/rustyn64-cpu/src/lib.rs
  • crates/rustyn64-cpu/src/pipeline.rs
  • crates/rustyn64-test-harness/src/rom.rs
  • crates/rustyn64-test-harness/tests/first_rom.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/cpu.md

Comment thread CHANGELOG.md
Comment thread crates/rustyn64-cpu/src/decode.rs
Comment thread docs/STATUS.md Outdated
doublegate added a commit that referenced this pull request Jul 21, 2026
Two functional, four inaccurate comments, two documentation.

LWU joins the 64-bit operations (CodeRabbit, #33). It was missing because the set
was built from n64-systemtest's 28 tested instructions, and the ROM does not
cover it. The manual states the rule once, as the EPSILON marker in the opcode
table: "valid in the 64-bit mode and 32-bit Kernel mode. In the 32-bit User or
Supervisor mode, this code generates the reserved instruction exception." Reading
that legend rather than the per-instruction notes is what found the gap, and the
doc comment now cites it so the next reader checks the same place. The oracle is
unchanged at Failed: 0, so this adds a rule without disturbing a measured one.

Index_Store_Tag now requires the cache's OWN valid PState -- 2 for the I-cache, 3
for the D-cache -- rather than any non-zero field (Copilot, #32). The old form
would conjure a live line from an undefined encoding, and would let one cache's
Valid validate the other's. Pinned by a test that walks the rejected values.

The four comments were each describing code that had moved on:

- The FpCommit::Single comment still said "preserves the upper half" while the
  code calls write_s_arith, which clears it. This is the fourth time in this
  project a comment has outlived its code; the rule it states was right and the
  attribution was not.
- The Index_* cache comment claimed segment translation leaves the index bits
  alone. False for every mapped segment -- translation preserves only the low 12
  bits, while the D-cache index reaches bit 12 and the I-cache bit 13. The real
  situation is the D-6 deviation, and it now says so.
- translate_re's Errors section named only TLB faults; it can also raise
  AddressError.
- The KSU == 3 fallback was described as "most restrictive-to-reach" when Kernel
  is the most permissive.

Documentation: D-6 no longer claims physical indexing is strictly more coherent
than the hardware -- it is a divergence in both directions, and the entry now
names the two observable cases and bounds the tested scope to KSEG0. The
STATUS.md gate row no longer reads "Failed: 0" as a label while reporting a
non-zero count in the same row.

The MOV.S test asserted only the low word, so a formatted half-copy would have
passed its whole-register claim (CodeRabbit, #31). It now asserts read_raw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate force-pushed the feat/vr4300-caches branch from 84ee3eb to 020f1b8 Compare July 21, 2026 04:30
doublegate and others added 19 commits July 21, 2026 00:56
Reversing endianness on a 64-bit datapath is a permutation of byte lanes within
the doubleword, expressed as an XOR of the low address bits: a doubleword does
not move, a word moves by 4, a halfword by 6 and a byte by 7. Kernel and
Supervisor are unaffected, and so is any access taken while EXL/ERL forces
kernel mode -- the same rule the coprocessor-usability check already applies.

Instruction fetch is swapped too, being a 4-byte access like any other. That is
why n64-systemtest emits its reverse-endian programs with each instruction PAIR
exchanged for them to execute in order.

The swap is applied to the PHYSICAL address, after translation. It touches only
bits 2:0, which every translation maps identically, so it is exactly equivalent
to swapping the virtual address first -- and it keeps BadVAddr raw on a fault,
which the suite asserts directly in its own test.

The LWL/LWR/SWL/SWR family under RE is deliberately NOT included: it addresses
individual bytes rather than a fixed-width container, so it needs a different
rule, and guessing one would be a fitted constant in another guise. Two
assertions still fail there and are recorded as outstanding in docs/cpu.md.

n64-systemtest, Phase 1's categories: 31 -> 27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g width

KSEG0 does not exist in User mode. The map was a function of the address alone,
so a user program reached 0x8000_1000 exactly as the kernel does -- and it is the
address-space check, not the TLB, that is supposed to stop it. The map is now a
function of (address, mode, width).

User sees USEG alone; Supervisor sees SUSEG and SSEG; Kernel sees the whole map.
Anything else is an address error raised BEFORE the TLB is consulted. That
distinction is load-bearing rather than cosmetic: folding it into a TLB refill
would send the offending program to the refill handler, where a well-behaved
kernel maps the page and grants the access it was never allowed to make. Hence a
separate TranslateError variant rather than a synthesised TlbFault.

With Status.KX/SX/UX set, each mapped segment widens to 2^40 and the space grows
holes -- an address inside a segment's region but past its size faults. Kernel
additionally gains XKPHYS: eight 2^32 direct windows chosen by bits 61:59,
differing only in cacheability, and by the same rule as a TLB entry's C field,
only C == 2 is uncached.

Under 32-bit addressing an address must be the sign extension of its low word.
0x0000_0000_8000_1000 is not a shorthand for KSEG0; it is an address error, and
n64-systemtest asserts that directly. The old code truncated to u32 and accepted
it silently -- and this project's own reset vector and ROM entry point were both
stored in the truncated form, so both are corrected here. Every kernel address in
the test suite needed the same correction, which is the honest reading: those
tests were passing on an address the hardware rejects.

One test moved rather than being adjusted: cop0_is_unusable_in_user_mode_without_cu0
ran its program from KSEG0 in User mode, which now raises AdEL at the FETCH -- so
it would have "passed" on the wrong exception. It maps page-pair 0 and runs from
USEG instead, which is the only place a user program can be.

n64-systemtest, Phase 1's categories: 27 -> 24.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rvisor mode

DADD, DSLL, LD, SD and the rest of the MIPS III doubleword set raise Reserved
Instruction when the current mode's UX/SX bit is clear and the mode is not
Kernel. Kernel may use them at any width, so this cannot be a property of
Status.KX alone.

Both halves of that condition are mutation-checked, because only one of them
fails loudly: gating on the width bit alone reserves them for a 32-bit KERNEL,
which is the mode every N64 boots into, so that mistake breaks everything. Gating
on the mode alone reserves them for a 64-bit USER program, which nothing common
does -- that one would sit unnoticed behind the rows that still pass.

Deliberately excluded: DMFC0/DMTC0 and DMFC1/DMTC1. Doubleword moves to and from
a coprocessor follow that coprocessor's own usability and reserved-encoding rules
(ledger C-18) and raise different exceptions; folding them in here would make an
already-unusable COP0 report the wrong cause.

The check runs after coprocessor usability, not before, so an unusable
coprocessor is still reported as such when the encoding is also a 64-bit one.

n64-systemtest, Phase 1's categories: 24 -> 23. The whole Privilege group passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… NOT

The failing assertion names ADD.S, and ADD.S is not the defect: running the
identical instruction in isolation with the ROM's own operands produces the
expected result in both FR modes. Recording that rules out the obvious suspect
before the next session spends the same effort re-deriving it.

The observed value is 16f32 in the high half with the low half cleared, which is
the signature of a single-precision write to an ODD register under FR = 0 -- and
the only instruction in the block that produces exactly that targets fgr[16].
That is a lead, not a conclusion, and the entry says so.

The status line names the next step as a correlated capture at the ROM's own
marker rather than further reasoning, because two rounds of reasoning have
already produced two wrong answers here -- the same trap C-10 recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the easy fix

A correlated capture armed on the ROM's own marker settles which instruction is
at fault, and it is not the one the assertion names: ADD.S $0 executes correctly,
and it is ADD.S $1, $29, $30 five rows later whose result is wrong. The pipeline
depth is exactly that gap, so the assertion index and the guilty instruction are
one apart -- which is what sent two earlier attempts to the wrong place.

The entry also records what the evidence RULES OUT, which is the more useful
half. The expected table is self-contradictory under every one-line index-remap
rule: result[1] needs $29 to read as $28, while result[2] needs $31 to read as
itself rather than as $30. And a companion failure shows FGR1 keeping its
untouched preload, so hardware writes an odd destination where we fold it into
the even pair. Sources and destinations need separate rules.

Naming that explicitly is the point: the next session should open the manual on
the FR description rather than do more arithmetic on the expected values, which
is where three rounds have already been spent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A floating-point arithmetic instruction ignores the low bit of fs and does NOT
ignore the low bit of ft. The destination fd is used as-is in both modes.

The manual declines to specify this -- an odd register with FR = 0 is
"undefined" (UM 7.5.3, 16) -- so the oracle is n64-systemtest's measured table
and the ledger records it as C-21, a measurement rather than documentation.

Two rows settle it, and no single mapping satisfies both:

  SQRT.S $13, $31  yields sqrt(16), so fs = 31 read FGR30.
  ADD.S  $2, $28, $31 yields -10 + -16, so ft = 31 read FGR31.

The suite then states it outright in its own assertion messages ("Lowest bit of
fs should be ignored", "Lowest bit of ft should not be ignored"), which is worth
recording because it means this was measurable rather than inferred.

This does NOT revise C-14. That entry governs MTC1/LWC1 and the doubleword
coprocessor moves, which really do reach an odd register's high half; it simply
does not extend to the arithmetic operand ports. Two mappings for two instruction
classes is surprising, so read_s_fs/read_s_ft exist as separate accessors and a
call site cannot silently pick the wrong one.

Getting the destination wrong is separately observable: folding an odd
destination into its even partner leaves the odd FGR holding its preload, which
the suite checks after ADD.D $1.

n64-systemtest, Phase 1's categories: 23 -> 19. Four of the five half-mode tests
pass; the fifth is a pipeline hazard, not an addressing bug, and is recorded as
residual R-2 rather than left to be rediscovered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fault

Three unrelated rules, each with a reason it stayed wrong.

PRId now reads 0x0B22. The Rev field was recorded as undocumented and left zero,
which was a true statement about the User's Manual and a false one about the
N64brew wiki this project mirrors -- it names 0x10, 0x22 and 0x40 for early,
later and iQue parts. Ledger U-3 is superseded by C-22. This is the third time a
decayed "undocumented" claim has been cited here as if it described the hardware,
which is exactly why engineering-lessons 3.3b exists: nothing fails when such a
claim goes stale, so it survives review.

Random is a plain 6-bit down-counter whose reload fires on == Wired, not
<= Wired, and whose decrement wraps 0 -> 63. The two readings agree for the
ordinary Wired <= 31 case and diverge only above it, which software can reach
because the field is six bits: the old reading pinned the register at 31 forever.
Ledger C-23. Worth noting the suite samples a RANGE to catch this -- a single
sample cannot tell a pinned counter from a slow one.

BadVAddr reports the address the instruction named, not the container address
translated on its behalf. A fault on SWL 0x12345001 reported 0x12345000.

n64-systemtest, Phase 1's categories: 19 -> 16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CVT.S.W, CVT.S.L, CVT.D.W and CVT.D.L were each a Rust `as` cast plus a
round-trip inexact check. `as` rounds to nearest-even unconditionally, so the
mode was ignored -- while the round-trip check still reported `inexact`
correctly. That combination is worth naming: the flags were right and the value
was wrong, so flag agreement gave no signal at all.

n64-systemtest converts 1234567891 toward zero and expects 0x4E93_2C05 where
nearest-even gives 0x4E93_2C06; CVT.D.L of 0x007F_FFFF_FFFF_FFFE toward zero
expects 0x435F_FFFF_FFFF_FFFF, not 0x4360_0000_0000_0000.

All four now go through softfloat::from_int, which is one line on top of the
shared rounding point: an integer is sign x |v| x 2^0, so it is round_pack with a
zero exponent and no sticky bit. Routing it through the same place as every other
operation is what makes the mode impossible to forget.

The four old converters are DELETED rather than left unused. An unused function
that quietly gets an operation wrong is the inert-API hazard engineering-lessons
3.2 describes, and addr.rs deleted a stale `translate` for exactly this reason.
long_convertible stays -- the VR4300 range restriction is a separate rule and is
still consulted. Ledger C-24.

n64-systemtest, Phase 1's categories: 16 -> 11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CVT.W.W, CVT.W.L, CVT.L.W, CVT.L.L and the whole ROUND/TRUNC/CEIL/FLOOR family
from .W/.L are not instructions. They were reaching the Cop1Unimplemented
fallthrough, which deliberately does not raise, so they retired silently --
n64-systemtest saw no exception where it expects Unimplemented Operation.

They now decode into the arithmetic path specifically so they reach the refusal
that already handles the subnormal and 2^53 cases. Widening the decode arm rather
than making the generic fallthrough raise keeps the trap machinery in one place,
and keeps the fallthrough's own contract intact -- there is a test asserting an
unimplemented COP1 encoding with CU1 set must NOT raise.

The refusal is checked BEFORE the source is read as a float. An integer source
format has no float to widen, and every branch below would happily reinterpret
the register and return a plausible number for an instruction that does not
exist -- which is precisely the shape of the bug being fixed.

n64-systemtest, Phase 1's categories: 11 -> 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PageMask bits 24:13 are six 2-bit pairs, and an entry does not store twelve
independent bits: a pair reads back as 11 exactly when its HIGHER bit was
written, and 00 otherwise. 0b10 becomes 0b11; 0b01 is discarded.

The natural implementation -- keep the value, mask it to 24:13 -- is wrong in
both directions and quietly: it accepts page sizes the hardware has no encoding
for, and it reports back a mask that was never stored. n64-systemtest writes
seventeen values and checks each read-back, including 0b00000000100 -> 0, where a
masking implementation returns its input unchanged.

Canonicalisation happens on write, because that is where the information is lost
-- the entry has nowhere to keep the discarded bits.

Mutation-checked: substituting `mask & 0x01FF_E000` turns the new test red. The
test's literals are grouped in PAIRS rather than nibbles, since the pair is the
unit the hardware stores and a mask that looks wrong that way is wrong.

n64-systemtest, Phase 1's categories: 7 -> 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EntryHi's tag keeps every bit PageMask does not cover. It was stored as
VA / pair_size, which clears the tag's LOW bits -- correct for all six legal page
sizes, since those are contiguous runs from bit 13, and wrong as soon as a
canonicalised mask has a hole. 0b11_11_11_11_00 covers bits 22:15 and leaves
14:13 alone, and no divisor expresses that.

The tag is now held in place and masked, so vpn2_of and the read-back agree by
construction rather than by two matching derivations. For every contiguous mask
the two are identical, which is why this survived until a test wrote a holed one.

Mutation-checked: restoring the divide turns the new test red on exactly the
holed-mask row and leaves the five contiguous rows green -- which is the useful
shape, since it shows the old code was not merely untested but untestable by any
legal page size.

n64-systemtest, Phase 1's categories: 6 -> 5. Every TLB item now passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Underflow was decided from the packed result -- subnormal or zero raised it, a
normal result did not. IEEE 754 permits tininess to be judged either before or
after rounding, and the VR4300 judges it BEFORE.

The two conventions differ exactly when a directed rounding mode lifts a tiny
result back into the normal range. FLT_MIN / (1 + 1ulp) under round-toward-+inf
yields FLT_MIN, a perfectly normal number, and hardware still raises underflow
because the value it rounded FROM was tiny. n64-systemtest carries that case in
both signs, and note what made it hard to spot: the result VALUE was already
correct, so only the flag disagreed.

The flag is now set once from the pre-rounding shift decision rather than
re-derived on each exit path. The mutation check takes down three tests instead
of one, which is the point -- the previous shape had two independent assignments
that could drift apart.

n64-systemtest, Phase 1's categories: 5 -> 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LWL/LWR/SWL/SWR and their doubleword siblings address individual bytes, so RE
moves them by addr ^ 7 rather than by their container's width. One XOR relocates
the container and complements the byte index together: LWL 0 becomes container 4
with byte index 3, since 0 ^ 7 == 7, 7 & !3 == 4 and 7 & 3 == 3.

This was left deliberately open when RE landed, because the width-based rule
plainly did not apply and any replacement would have been fitted rather than
derived. It is derived now, from the ROM's own expected tables: SWL at offset 0
writes a single byte -- rt's most significant -- into the doubleword's LAST byte,
which no width-based swap produces, and reading the table settled in minutes what
computing against it had not.

BadVAddr still reports the untransformed address, since the swap is applied to a
separate effective address and the fault path already overrides it.

n64-systemtest, Phase 1's categories: 3 -> 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nted

The previous entry diagnosed this as a pipeline hazard: FCSR.C commits in WB
while the branch resolves in EX, so the branch reads a stale condition. That
reading was derived from the pipeline's structure without first checking that the
branch existed. It does not.

A per-cycle trace shows BC1T decoding to Cop1Unimplemented and retiring as a
no-op, so the flag the test reads is never overwritten by anything. pipeline.rs
already said so in as many words -- "a real, valid COP1 branch that is genuinely
not wired yet", with a note to move that test when BC1 lands -- and I wrote a
hazard diagnosis without reading it.

Worth recording as more than a correction: R-2's own status line said to trace
rather than reason, and the reasoning that produced the wrong entry happened
before the trace. The rule only helps if it runs first.

The entry now names the concrete work (four Op variants, decode from COP1
rs = 0o10 with bits 17:16 as nd:tf, is_likely for the *L pair, and the condition
plumbed into execute, which today cannot see FCSR) and keeps the hazard as a
SECOND question -- the same trace does show FCSR.C committing two cycles after an
adjacent branch would reach EX, so an interlock is probably still needed once the
branch exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
They were not implemented at all. The branch decoded to Cop1Unimplemented and
retired as a no-op, so a program branching on a compare simply fell through --
and the test that pinned this said so in as many words, with an instruction to
move it when BC1 landed. Moved rather than deleted, onto COP1 rs = 0o11, which is
genuinely unassigned.

COP1 rs = 0o10, bits 17:16 as nd:tf, so the four encodings are true/false crossed
with likely/not. Target arithmetic and branch-likely nullification are shared with
every other branch rather than duplicated.

FCSR.C is passed into `execute` as a parameter rather than reached for: that
function is pure and has no view of coprocessor state, and a parameter makes
every call site a compile error until it supplies one -- which is what keeps a
defaulted or stale condition from being wired in silently.

STILL OUTSTANDING, and now the only Phase 1 failure: BC1 reads the condition in
EX while C.cond.fmt writes it in WB, so an adjacent pair samples the previous
condition. Hardware interlocks; we do not yet. A first interlock attempt is
recorded in ledger R-2 together with why it is insufficient -- it fires once and
is then satisfied while the commit still has not happened. The remaining fix must
come from tracing where the compare actually is on each cycle, not from choosing
a stall count that makes the ROM pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed out

A per-cycle trace pins it: with C.EQ.S at cycle 1, BC1T executes EX at cycle 3
while the compare's WB runs at cycle 4. The branch is one cycle short.

The obvious fix does not work, and that is the useful part of this entry.
stall_for freezes the WHOLE pipeline, so holding the branch delays the compare's
WB by exactly the same amount and the branch never catches up -- an interlock on
ex_dc/dc_wb was implemented, fired once, and changed nothing. That interlock has
been removed rather than left in: a guard with the shape of a fix but no effect
is worse than none, because it makes the hazard look handled.

A bypass is also unavailable as things stand: fp_arith COMPUTES the condition in
WB, not EX, so there is no pending value to forward.

Two viable routes are recorded instead of a guess: a draining bubble that holds
IC/RF/EX while DC/WB continue, or moving the condition computation to EX and
leaving only the commit in WB so it can be bypassed like a GPR. The second
touches ADR 0007's single commit point and would need writing up, so the choice
is deliberate rather than incidental.

Not doing: picking a stall count that makes the ROM pass. That is the fitted
timing constant this ledger exists to prevent -- every later timing result built
on it would stop being evidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This clears Phase 1's cut criterion: n64-systemtest's CPU/COP0/TLB/COP1
categories are at Failed: 0.

BC1 resolves in EX while C.cond.fmt commits FCSR.C in WB, so an adjacent pair
sampled the previous condition -- and the ROM emits exactly that pair with no
separating instruction.

Fixed by a forwarding path rather than a stall, and the distinction was measured
rather than assumed. stall_for freezes every stage, so holding the branch delays
the compare's WB by the same number of cycles and the gap never closes; an
interlock on ex_dc/dc_wb was written and traced, fires once, is satisfied while
the commit still has not happened, and changes nothing. The load interlock is not
a counter-example: it works because its consumer reads through the bypass
network, so the one-cycle stall buys DC time and forwarding does the rest. The FP
condition had no forwarding path at all, which is what this adds.

Re-evaluating the pending compare is sound because it reads two FP registers and
writes only FCSR.C -- nothing between it and the branch can change those
registers, since a branch has no destination. Flags are discarded: a forwarding
path must not raise the compare's trap on the branch's behalf. ex_dc is consulted
before dc_wb because it holds the younger instruction.

Mutation-checked in both directions. Removing the bypass reddens the test, and so
does a bypass that always reports "true" -- the second matters because a test of
the taken case alone would pass against it.

Ledger C-25; R-2 closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The state section still said "Phase 1 in progress" with 40 failing assertions and
BC1 decoding to a no-op. It now records the criterion as MET (Failed: 0 in
CPU/COP0/TLB/COP1), that the ~413 remaining are RSP/RCP and belong to Phase 2,
and that v0.2.0 is unblocked but deliberately NOT tagged -- tagging is the
VERSION-PLAN phase-close ceremony and belongs on main.

Also records a gap worth a ticket rather than a fourth rewrite: there is no
committed n64-systemtest runner, so every session so far has rebuilt a throwaway
one for the project's primary oracle.

Six conventions added, each from a bug this session shipped or nearly shipped:

- Agreeing flags are not evidence the value is right, and vice versa -- three
  bugs had exactly one half correct, and in two of them the correct half is what
  made the wrong half look fine.
- An address in 32-bit mode must be the sign extension of its low word.
- The segment map is a function of (address, mode, width), and an out-of-range
  address raises AdEL BEFORE the TLB is consulted.
- Read the oracle's table; do not compute against it.
- A stall cannot substitute for a bypass when the stall freezes both sides.
- Delete a superseded function rather than leaving it unused.

The "undocumented decays" entry gains its third instance (PRId.Rev, documented in
the wiki this repo mirrors) and a sharper rule: name the source, because "the UM
does not say" is checkable and "undocumented" is not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two functional, four inaccurate comments, two documentation.

LWU joins the 64-bit operations (CodeRabbit, #33). It was missing because the set
was built from n64-systemtest's 28 tested instructions, and the ROM does not
cover it. The manual states the rule once, as the EPSILON marker in the opcode
table: "valid in the 64-bit mode and 32-bit Kernel mode. In the 32-bit User or
Supervisor mode, this code generates the reserved instruction exception." Reading
that legend rather than the per-instruction notes is what found the gap, and the
doc comment now cites it so the next reader checks the same place. The oracle is
unchanged at Failed: 0, so this adds a rule without disturbing a measured one.

Index_Store_Tag now requires the cache's OWN valid PState -- 2 for the I-cache, 3
for the D-cache -- rather than any non-zero field (Copilot, #32). The old form
would conjure a live line from an undefined encoding, and would let one cache's
Valid validate the other's. Pinned by a test that walks the rejected values.

The four comments were each describing code that had moved on:

- The FpCommit::Single comment still said "preserves the upper half" while the
  code calls write_s_arith, which clears it. This is the fourth time in this
  project a comment has outlived its code; the rule it states was right and the
  attribution was not.
- The Index_* cache comment claimed segment translation leaves the index bits
  alone. False for every mapped segment -- translation preserves only the low 12
  bits, while the D-cache index reaches bit 12 and the I-cache bit 13. The real
  situation is the D-6 deviation, and it now says so.
- translate_re's Errors section named only TLB faults; it can also raise
  AddressError.
- The KSU == 3 fallback was described as "most restrictive-to-reach" when Kernel
  is the most permissive.

Documentation: D-6 no longer claims physical indexing is strictly more coherent
than the hardware -- it is a divergence in both directions, and the entry now
names the two observable cases and bounds the tested scope to KSEG0. The
STATUS.md gate row no longer reads "Failed: 0" as a label while reporting a
non-zero count in the same row.

The MOV.S test asserted only the low word, so a formatted half-copy would have
passed its whole-register claim (CodeRabbit, #31). It now asserts read_raw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate force-pushed the feat/addressing-and-privilege branch from d33fbef to 419962e Compare July 21, 2026 04:57
@doublegate
doublegate changed the base branch from feat/vr4300-caches to main July 21, 2026 04:57
@doublegate doublegate changed the title feat(cpu): Status.RE, the privilege-aware segment map, and 64-bit reserved encodings feat(cpu): Status.RE, privilege-aware addressing, and the COP1 residuals that close Phase 1 Jul 21, 2026
doublegate and others added 2 commits July 21, 2026 01:03
CodeRabbit's finding named two sites; I fixed one. `docs/accuracy-ledger.md` D-6
and the `cache.rs` module doc were rewritten to say physical indexing is a
divergence in both directions, and `docs/cpu.md` was left still framing it as an
improvement -- "makes cache aliases impossible rather than merely unlikely",
which reads as strictly better and discourages the next reader from looking.

It now names the same two observable cases as D-6 (a deliberately constructed
alias, and an Index_* operation on a TLB-mapped page, where translation preserves
only the low 12 bits while the D-cache index reaches bit 12 and the I-cache bit
13) and states the tested scope as KSEG0.

Worth recording as a pattern rather than a typo: a claim asserted in three places
gets corrected in the two the author is looking at. The bounded version now reads
the same in all three.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found by auditing for the pattern CodeRabbit's last finding exposed: a claim
asserted in several places gets corrected in the ones the author happens to be
looking at. `Op::Cache`'s doc comment was the fourth site and still read
"executed as an address-translating no-op: this CPU does not model cache
contents" -- true under ledger D-5, false since T-11-003 landed the caches in
this same stack.

It now states what the instruction does, records that the old wording was
accurate under D-5, and points at D-6 which supersedes it. Keeping the retraction
visible rather than silently swapping the text, because the next reader is more
likely to trust a doc that shows its own correction.

The audit also confirmed the other three sites (accuracy-ledger D-5/D-6,
docs/cpu.md, cache.rs module docs) already agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/rustyn64-cpu/src/cache.rs (1)

236-242: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Uncited invented side effect: store_tag clears dirty.

The module's own docs (lines 41-43) state the write-back bit has no TagLo field and is invisible to Index_Load_Tag/Index_Store_Tag. Clearing dirty here on a valid, dirty line silently discards a pending write-back with no citation, no ledger entry, and no test — this is exactly the invented-side-effect pattern the path instructions single out as worse than an invented constant, because it never lands where it can be argued with. Icache::store_tag (lines 382-387) makes no equivalent assumption, so this isn't a shared convention either.

Either cite a source for this behaviour (or record it in docs/accuracy-ledger.md with a covering test), or drop the assignment and let the existing dirty state survive the tag overwrite.

🐛 Proposed fix
     pub const fn store_tag(&mut self, addr: u32, tag_lo: u32) {
         let i = Self::index(addr);
         let (tag, valid) = unpack_tag(tag_lo, DCACHE_VALID_STATE);
         self.lines[i].tag = tag;
         self.lines[i].valid = valid;
-        self.lines[i].dirty = false;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustyn64-cpu/src/cache.rs` around lines 236 - 242, Remove the
self.lines[i].dirty = false assignment from Dcache::store_tag so Index_Store_Tag
preserves the existing dirty state, matching Icache::store_tag and the
documented TagLo behavior. Do not add new side effects or alter the tag and
valid updates.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/accuracy-ledger.md`:
- Around line 1010-1028: Update the earlier U-3 ledger row to mark it resolved
or superseded by C-22, preserving its original historical wording and adding a
reference to C-22. Do not alter the new C-22 entry or its technical conclusions.

In `@docs/cpu.md`:
- Around line 509-533: Attach authoritative provenance to each newly documented
hardware rule: in docs/cpu.md lines 509-533, cite a manual, wiki, or
accuracy-ledger measurement for privilege-aware segment mapping, XKPHYS
behavior, and sign extension; in AGENTS.md lines 306-315, cite the source for
sign-extension and segment-map conventions; in docs/cpu.md lines 643-674, cite
the source for integer-source conversion refusal and 32-bit-mode doubleword
reservation; and in docs/cpu.md lines 692-696, cite the measured or documented
basis for the byte-granular Status.RE rule.

In `@docs/STATUS.md`:
- Line 181: Reconcile the execution-status claims across docs/STATUS.md lines
181-181, AGENTS.md lines 62-76 and 89-91, and CHANGELOG.md lines 12-18: do not
publish “Failed: 0” or mark the criterion cleared without reproducible oracle
output. Remove or correct the asserted result, update the stale COP1-failure
text consistently, and replace the “no committed runner” statement only if a
canonical runner actually exists.

---

Outside diff comments:
In `@crates/rustyn64-cpu/src/cache.rs`:
- Around line 236-242: Remove the self.lines[i].dirty = false assignment from
Dcache::store_tag so Index_Store_Tag preserves the existing dirty state,
matching Icache::store_tag and the documented TagLo behavior. Do not add new
side effects or alter the tag and valid updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c2f2e097-20e5-40be-a980-c8be6421e21d

📥 Commits

Reviewing files that changed from the base of the PR and between c3c9403 and 6bdbbaa.

📒 Files selected for processing (18)
  • AGENTS.md
  • CHANGELOG.md
  • crates/rustyn64-cpu/src/addr.rs
  • crates/rustyn64-cpu/src/cache.rs
  • crates/rustyn64-cpu/src/cop0.rs
  • crates/rustyn64-cpu/src/decode.rs
  • crates/rustyn64-cpu/src/exec.rs
  • crates/rustyn64-cpu/src/fpr.rs
  • crates/rustyn64-cpu/src/fpu.rs
  • crates/rustyn64-cpu/src/lib.rs
  • crates/rustyn64-cpu/src/pipeline.rs
  • crates/rustyn64-cpu/src/softfloat.rs
  • crates/rustyn64-cpu/src/tlb.rs
  • crates/rustyn64-test-harness/src/rom.rs
  • crates/rustyn64-test-harness/tests/first_rom.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/cpu.md

Comment thread docs/accuracy-ledger.md
Comment thread docs/cpu.md
Comment thread docs/STATUS.md Outdated
…ducible

CodeRabbit flagged docs/STATUS.md for publishing `Failed: 0` while AGENTS.md said
in the same breath that no runner existed. It was wrong that the oracle had not
run -- it has, repeatedly -- and right about what matters: the result was not
reproducible from the repository, so nobody could check it. A measured claim
nobody can re-run is a self-assessment wearing a measurement's clothes, and the
correct answer is to make it checkable rather than to soften the wording.

The runner is now a committed `#[ignore]`d harness test (~2 min in --release, far
too slow for the default path). The ROM is already committed under MIT, so it
needs no external corpus. It asserts Phase 1's criterion directly -- the
CPU/COP0/TLB/COP1 categories, matching on what to EXCLUDE so a new CPU-side
category added upstream lands inside the gate rather than being silently ignored.

It witnesses execution before trusting a zero. Writing that guard immediately
caught my own first version, which asserted on a count of FAILURE lines: 413
failures read as "the suite did not complete" when the suite had in fact run all
917 tests. Counting failures cannot witness a run, because zero of them is
exactly what a run that never started also produces -- the vacuous-pass hazard,
which bit the guard meant to prevent it.

Also from the same review: U-3 is closed against C-22 with its original wording
preserved, and provenance is attached to the hardware rules documented this
phase -- the epsilon legend for the 64-bit reservation, the specific
n64-systemtest cases for sign extension, the integer-source conversion refusal,
and the byte-granular Status.RE rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit fd7014e into main Jul 21, 2026
8 of 9 checks passed
@doublegate
doublegate deleted the feat/addressing-and-privilege branch July 21, 2026 07:34
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.

2 participants