Skip to content

Reserved reference slots in the storage grid - #4

Merged
halaprix merged 4 commits into
masterfrom
feat/reserved-reference-slots
Jul 30, 2026
Merged

Reserved reference slots in the storage grid#4
halaprix merged 4 commits into
masterfrom
feat/reserved-reference-slots

Conversation

@halaprix

Copy link
Copy Markdown
Owner

The defect

A mapping, dynamic array, bytes or string reserves a slot that the grid never
showed. uint256 a; mapping(address => uint256) balances; uint256 b; rendered
slots 0 and 2 with nothing between them — read the grid top to bottom and the
compiler appears to have skipped an address. buildRows built its slot list only
from materialized value cells, so the reserved slot survived as prose under a
"Not expanded" heading.

That was the one place the instrument was silent instead of honest, and it was
silent about the most-taught fact in Solidity storage: a mapping consumes a slot
it then never writes, because its entries live at keccak(key ‖ slot).

What this does

Every reserved slot gets its own labeled row, and the row says what the slot
actually holds — which differs by encoding, so one shared sentence would be a lie:

Encoding Base slot holds Row shows
mapping nothing, ever — solc never writes the head RESERVED · mapping, no value
dynamic_array the length LENGTH · array, length = N
bytes/string the length and, under 32 bytes, the data itself short form renders the value inline

After a run, the observed word renders in amber — and only there, per the identity
law that amber marks measured values. A nonzero mapping head is flagged as an
anomaly rather than presented as a value, since only inline assembly or a slot
collision can put one there.

Rows are gapless by construction, not by assertion: buildRows walks a sorted
list of slot claims, collapses an unexpanded multi-slot span into a single elided
range instead of thousands of rows, and fills any unclaimed run with an honest
slots N–M · no declared variable banner. A 300-run fast-check property holds the
invariant.

Notable decisions

  • CollapsedStorageSpan carries solc's encoding. The UI previously decided a
    span was a mapping via typeLabel.startsWith('mapping') — a heuristic over a
    display string, which this codebase forbids. An encoding solc does not emit now
    throws rather than being treated as one of the four.
  • The row shape stayed one shape. A discriminated row union was the obvious
    alternative and the wrong trade: roving focus, pinning, React keys and
    aria-rowcount all index through row.slot/row.segments. Adding endSlot
    plus two full-width segment kinds left all four untouched.
  • Observations, not a state replay, feed the measured word — so a slot that was
    only read still shows a value, which is the only way pre-existing state becomes
    visible. Documented limit: these are last-observed words, not post-revert final
    state.
  • The default editor source seeds from the golden fixture (App.tsx:16), so
    the fixture gains balances/history/note at slots 5–7 plus a constructor
    that fills two of them. This is the one deliberate golden change, made through
    the reviewed --update path. Every existing variable kept its exact slot and
    offset; sourceHash, expectedLayoutHash and expectedRelationHash moved,
    expectedInstructionHash did not.
  • Keccak-derived slots stay out of the grid (slotscope-xu8, v1.4). A row for
    one would claim an adjacency near 2^256 that it does not have, and the gapless
    walk would need an elided range ~10^76 slots wide to reach it.

Also fixed

CodeMirror's floating widgets — the lint tooltip, the diagnostic list, the Ctrl-F
panel — were rendering with the library's light-theme defaults under the app's
light text, effectively unreadable. An app-level .cm-tooltip rule does not fix
it (CodeMirror injects its base styles as a StyleModule, so the rule ties on
specificity and loses on order — measured, the tooltip stayed
rgb(245, 245, 245)). They are themed through EditorView.theme marked dark.

Two things the gates caught

  • The longer fixture pushed positionValue out of CodeMirror's virtualized
    viewport, so relations.spec.ts could not find the line it clicks. The new
    declarations moved below the function; declaration order still assigns slots
    5–7.
  • decode.test.ts asserted items.every(kind === 'value') on the fixture, now
    false. Replaced with a stronger check: the three reference types collapse at
    slots 5–7 with the right compiler encodings.

Verification

Gate Result
format:check, lint, typecheck clean
test:unit 235/235, including the Forge 1.5.1 cross-check
fixtures:verify no drift after the reviewed --update
test:e2e 61/61 — axe in both motion modes, roving focus, 200% zoom, pins
build + build:check pass, 224 KB main entry
benchmark:v0.1 p95 225 ms action-to-paint, 657 ms cold start — PASS

Design spec: docs/design/reserved-slots-v1.2.md. Plan:
docs/plans/2026-07-30-reserved-reference-slots.md. Closes slotscope-zvd
(.1.5) and slotscope-73y.

One thing worth a manual look: the Ctrl-F search panel is themed but no test
opens it.

🤖 Generated with Claude Code

halaprix and others added 3 commits July 30, 2026 14:56
The storage grid silently skips the slot a mapping, dynamic array, bytes or
string reserves. Record the design that gives every reserved slot a row, and
the task-by-task plan that implements it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
app.css themed .cm-editor, .cm-gutters and .cm-line but nothing floating, so
the lint tooltip, the diagnostic list and the Ctrl-F panel kept CodeMirror's
stock light background under the app's light text — bright on bright, and
effectively unreadable.

A plain .cm-tooltip rule in app.css does not fix this: CodeMirror injects its
base styles as a StyleModule, so an app-level rule ties on specificity and
loses on order (measured: the tooltip stayed rgb(245, 245, 245)). Use the
supported mechanism instead — EditorView.theme marked dark, which CodeMirror
injects after its base sheet. `dark: true` also gives dark defaults to widgets
not named here, so enabling one more extension cannot bring the bug back.

The new e2e hovers a real lint marker, asserts the computed background, and
runs axe with the tooltip open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A mapping, dynamic array, bytes or string reserves a slot that buildRows never
emitted, because its slot list came only from materialized value cells. So
`uint256 a; mapping(...) b; uint256 c;` rendered slots 0 and 2 with nothing
between them, and the reserved slot survived only as prose under "Not expanded".

buildRows now walks a sorted list of slot claims. A reference head — always one
32-byte word, always alone in its slot — becomes its own row. An unexpanded
inplace span becomes one elided range row rather than thousands of rows, and any
unclaimed run becomes an honest `slots N-M - no declared variable` banner, so the
row list is gapless by construction rather than by assertion. A 300-run
fast-check property holds the invariant.

Each row says what its slot actually holds, which differs by encoding: a mapping
head is never written, an array head holds the length, and a bytes/string head
under 32 bytes holds the data itself — so "reserved, nothing here" would be false
for two of the three. After a run the observed word renders in amber, and only
there; a nonzero mapping head is flagged as an anomaly, since only assembly or a
slot collision can put a value in one.

Supporting changes:

- CollapsedStorageSpan carries solc's `encoding`, replacing classification by
  typeLabel.startsWith('mapping'). An encoding solc does not emit now throws.
- runtimeView folds trace observations into slot -> word (reads included, so
  pre-existing state is visible) plus distinct derived-entry counts per base.
- The default editor source seeds from the golden fixture, which had no
  reference type, so the fixture gains balances/history/note at slots 5-7 and a
  constructor that fills two of them. Declared after positionValue: the editor
  virtualizes lines and relations.spec.ts clicks one inside that function.
  Goldens regenerated through the reviewed --update path; every existing
  variable kept its exact slot and offset.
- The "Not expanded" list is retired; its teaching copy moved onto the rows and
  the per-slot record. The e2e assertion that the mapping appears only in that
  list encoded the defect and is replaced deliberately.

Keccak-derived slots stay out of the grid (slotscope-xu8, v1.4): a row for one
would claim an adjacency near 2^256 that it does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slotscope Ready Ready Preview Jul 30, 2026 1:02pm

…gate

release:verify scans for raw color literals outside tokens.css and does not
exempt comments. The comment cited CodeMirror's stock background by hex as
evidence; the point survives without the number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@halaprix
halaprix merged commit da4ed3b into master Jul 30, 2026
8 checks passed
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