Skip to content

ruff_r2il: fix the structurally-empty data_refs column - #110

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/c64-6502-falsifier-shztkk
Aug 26, 2026
Merged

AdaWorldAPI merged 1 commit into
mainfrom
claude/c64-6502-falsifier-shztkk

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

reachability_probe collected static data references with addr.space == Const && in_image(addr.offset). That filter cannot match on this architecture, so unreached+ref printed 0.00% for every image and looked like a measurement.

A full linear sweep of LOCODE.unprot.bin counts 1418 load/store address operands:

space count in image
Unique 860 —
Register 328 —
Const 230 0

Why — it took a p-code dump to see

At $1d27, a 3-byte STA $ce00,y lifts to:

IntZExt { dst: Unique 7680, src: Register 1 }                 ; widen the index
IntAdd  { dst: Unique 7936, a: Const 52736, b: Unique 7680 }  ; base + index
Store   { addr: Unique 7936, val: ... }                       ; address is a TEMP

The data address is Const 52736 = $ce00 — the constant operand of the IntAdd that defines the address temp, never the address operand itself.

The near-identical shape at $1d2d is the one that must not count:

IntSub { dst: Unique 19584, a: Register 34, b: Const 1 }      ; stack pointer - 1
Store  { addr: Unique 19584, val: Const 7472 }                ; a stack push

Same silhouette, and it carries a constant too — so a pass that grabbed "the constant near the store" would emit $0001 as a data address. Worse than emitting nothing, because it would look like a measurement.

The fix

src/absref.rs resolves the address through the block:

  • Const operand → the address directly (zero page, hardware vectors).
  • Unique → the nearest preceding op that writes that temp (via R2ILOp::output()), accepted only when it's an IntAdd carrying a constant.
  • anything else → nothing. An indirect Register operand needs a value analysis and is not guessed at.

Nearest-preceding is what makes this correct rather than approximate: 6502 blocks reuse temp offsets freely — offset 1920 is reassigned nine times in one BRK block — so taking the first definition would read a stale base. Pinned by its own falsifier.

One level deep on purpose. A Copy chain into the address temp would also be resolvable and is cheap to add; it's absent because it wasn't observed on this corpus, and untested code that looks like it works is worse than an honest gap.

Six hermetic tests, both real measured shapes among them, so neither half can pass by being trivially empty.

Measured

image refs unreached+ref was
LOCODE.unprot.bin 9 0.08% 0.00%
HICODE.unprot.bin 51 0.20% 0.00%

The raw reference count is now its own column. One number could not distinguish "the resolver found nothing" from "it found plenty and they were all reached code" — and the first of those is what the column silently was.

Read together they say something reached% alone cannot: of 60 in-image referenced addresses, 58 are never reached as code (LOCODE 7 of 9, HICODE 51 of 51). That's the "data is what is only ever loaded from" half of the hypothesis, which had no evidence behind it until now.

mgra_emit consumes the same pass — referenced nodes go 4 → 38 and 10 → 126. The verdict, the CODE/DATA ranges, and the emitted graph's call side are all unchanged.

Gates

cargo clippy --features lift --all-targets -- -D warnings clean · cargo fmt --check clean · cargo test -p ruff_r2il --lib 52/52 · prek on every changed file · both probes re-run end to end.


Generated by Claude Code

reachability_probe collected static data references with
`addr.space == Const && in_image(addr.offset)`. That filter cannot match on this
architecture, so `unreached+ref` printed 0.00% for every image and looked like a
measurement. A full linear sweep of LOCODE counts 1418 load/store address operands —
860 Unique, 328 Register, 230 Const — and not one Const operand inside the image.

The reason is in the lift, and it took a p-code dump to see. At $1d27, a 3-byte
STA $ce00,y becomes:

    IntZExt { dst: Unique 7680, src: Register 1 }
    IntAdd  { dst: Unique 7936, a: Const 52736, b: Unique 7680 }
    Store   { addr: Unique 7936, val: ... }

The data address is the constant operand of the IntAdd that defines the address temp,
never the address operand itself. The near-identical shape at $1d2d is a stack push
(IntSub of the stack pointer and Const 1, then Store through the temp) and must not
count — it carries a constant too, so a pass that grabbed "the constant near the store"
would emit $0001 as a data address.

src/absref.rs resolves the address through the block: a Const operand directly, or the
nearest preceding op that writes the address temp, accepted only when it is an IntAdd
carrying a constant. Nearest-preceding is what makes it correct rather than approximate
— 6502 blocks reuse temp offsets freely, so taking the first definition would read a
stale base. Anything else resolves to nothing; an indirect Register operand needs a
value analysis and is not guessed at. One level deep on purpose: a Copy chain would
also be resolvable, and is absent because it was not observed on this corpus.

Six hermetic tests, both real shapes among them, so neither half can pass by being
trivially empty.

Measured, replacing 0.00% everywhere:

  LOCODE   refs 9   unreached+ref 0.08%
  HICODE   refs 51  unreached+ref 0.20%

The raw reference count is now its own column. One number could not distinguish "the
resolver found nothing" from "it found plenty and they were all reached code", and the
first of those is what the column silently was. Read together they say something the
reached% column cannot: of 60 in-image referenced addresses, 58 are never reached as
code — the "data is what is only ever loaded from" half of the hypothesis.

mgra_emit consumes the same pass; its referenced-node count goes 4 -> 38 and 10 -> 126.
The verdict, the ranges and the emitted graph's call side are all unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71545a10-0ff0-423f-902e-ce81c2aaaa6e


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_a1bb3a3a-00c6-4080-a100-ff78986b0a8c)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review August 26, 2026 07:47
@AdaWorldAPI
AdaWorldAPI merged commit c1d7c03 into main Aug 26, 2026
32 of 34 checks passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 868382cd12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +66 to +67
(SpaceId::Const, _) => Some(a.offset),
(_, SpaceId::Const) => Some(b.offset),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 1. Reject register-relative IntAdd operands

When an address temporary is defined by IntAdd(Register, Const)—for example, a 6502 stack pull that increments the stack pointer—these arms return the constant as an absolute base. This emits a false $0001-style AbsRef in mgra_emit, and a sufficiently large register offset can also pollute reachability_probe's in-image measurement; the existing stack test only covers the IntSub direction. Restrict the accepted nonconstant operand to the indexed-address shape and add a focused register-plus-constant falsifier.

AGENTS.md reference: AGENTS.md:L169-L170

Useful? React with 👍 / 👎.

Comment on lines +82 to +85
let defining = ops[..at.min(ops.len())].iter().rev().find(|op| {
op.output()
.is_some_and(|o| o.space == SpaceId::Unique && o.offset == addr.offset)
})?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 2. Match the complete varnode identity

When a block reuses a Unique offset at a different width, this search can select that unrelated writer instead of the definition of addr, either returning its base or prematurely returning None. The crate explicitly documents varnode identity as (space, offset, size) in facet.rs, so the predicate must compare size as well as space and offset before treating an output as the address definition.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
// A fully constant address: rare, but it is still an address.
(SpaceId::Const, SpaceId::Const) => Some(a.offset.wrapping_add(b.offset)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 3. Wrap constant sums to the p-code width

For the explicitly supported fully constant IntAdd case, arithmetic must wrap at the destination varnode's width, not at 64 bits. For example, a 2-byte p-code addition of 0xffff + 1 names address 0x0000, while this returns 0x10000, causing the reference to be assigned to the wrong node or filtered out as out-of-image. Pass the result width into this calculation and mask or truncate accordingly.

Useful? React with 👍 / 👎.

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