Skip to content

Collapse the mirror: LibUint256Array/Matrix become cast wrappers over the bytes32 libraries - #149

Merged
thedavidmeister merged 2 commits into
mainfrom
2026-08-18-issue-128-collapse-mirror
Aug 18, 2026
Merged

Collapse the mirror: LibUint256Array/Matrix become cast wrappers over the bytes32 libraries#149
thedavidmeister merged 2 commits into
mainfrom
2026-08-18-issue-128-collapse-mirror

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Refs #128 — option 1, the source collapse.

Direction

bytes32 keeps the implementation; LibUint256Array and LibUint256Matrix become cast wrappers over LibBytes32Array/LibBytes32Matrix. That is the reverse of #128's wording, for two reasons: bytes32 is the hot path in this stack, so the wrapper surcharge lands on the colder side, and bytes32 is the opaque-bits type with uint256 as the numeric view over it rather than the other way round. It also leaves the audited assembly where Protofire read it.

Each delegation does its own relabel in an inline assembly block at the point it needs one. There are no private cast helpers: a helper whose whole body is relabelled := array buys a call frame to move a pointer between two types that are the same structure in memory, and a jump is worth paying for real logic. Dropping the helpers also drops the two named returns they declared, which are not this repo's convention (#135).

The diff is two files, +174/−296. LibBytes32Array.sol, LibBytes32Matrix.sol and all 14 mirrored test files are untouched.

Measured, not asserted

Pinned toolchain (forge 1.7.2-nightly 43923a4, solc 0.8.25, optimizer on at 100000 runs, evm_version = "cancun"), bytecode_hash = "none" and cbor_metadata = false so no metadata trailer lands in the compared bytes.

The rig is a pair of mirrored harness contracts, one per element type, with one external function per library entry point — 26 each (17 array, 9 matrix) — so nothing is inlined into a caller and every entry point is separately measurable. A probe is the gasleft() span across a staticcall into one harness function. Calldata is built from uint256 values for both element types, because the ABI encoding of bytes32[] and uint256[] is byte identical, so the two sides receive the same bytes modulo the four byte selector. 16 of the 26 entry points vary with length and are swept at 0, 1, 2, 8 and 32; the other 10 get one probe each: 90 probes per element type.

All three builds — main, the private-helper spelling, and this inline-assembly spelling — were rebuilt from clean and re-measured against that one harness, so the figures below are comparable to each other. They are not comparable to the absolute numbers in the previous revision of this description, which came from a different rig and have been replaced rather than carried over. Every build and every probe run was repeated a second time from clean: byte-identical bytecode and byte-identical probe output both times.

main private cast helpers inline assembly (this branch)
Bytes32Harness deployed bytes 3947 3947 (+0) 3947 (+0)
Uint256Harness deployed bytes 3951 4327 (+376) 4423 (+472)
bytes32 gas, 90 probes +0 on all 90 +0 on all 90
uint256 gas, 90 probes +29…+129, mean +49.9 +0…+119, mean +51.3

Bytes32Harness runtime bytecode is byte-identical across all three builds — sha256 6fec3349256ddbd9cda812b7a10b4663db3de13ec3a06891fe247af5f014a78f — and its 90 gas probes are identical, gas for gas, on every one. The hot path is provably unmoved.

The uint256 surcharge is a flat constant per entry point: for all 16 size-varying entry points the delta against main is the same at every one of the five lengths, in both spellings. It is the wrapper failing to inline, not a copy.

Inline assembly against the private helpers, head to head

The spelling this branch ships is the dearer of the two on this rig, and the numbers are reported as they came out.

inline vs helpers
Uint256Harness deployed bytes +96
probes where inline is cheaper 29 of 90
probes where inline is dearer 61 of 90
probes where they are equal 0 of 90
per-probe spread −129 … +28, mean +1.4
entry points cheaper / dearer 9 / 17

The mean is a wash — 1.4 gas on entry points costing 1.0k to 37k — but the distribution is lopsided the other way from an earlier measurement of an earlier rig, which had inline cheaper on 50 of 90. On this rig it is dearer on 61 of 90. The cheaper side is concentrated in the wide arrayFrom overloads (arrayFrom8 −129, arrayFrom7 −81, arrayFrom5 −71); the dearer side is a uniform +4…+8 across the pointer, truncate, reverse and itemCount entry points, plus +23/+28 on matrixFrom2/matrixFrom3. The relabel itself is a pointer move in both spellings, and the delta against main is flat across the whole length sweep for every size-varying entry point, so what differs is how the legacy pipeline lays out the surrounding jumps rather than any work either spelling does.

The surcharge is accepted deliberately. rainlanguage/rainix#328 tracks the pipeline fix: under via_ir the wrapper compiles away entirely and the collapse is byte-identical to main.

The harness is a measurement rig and is not committed — the diff stays source-only. It is described above in enough detail to rebuild.

Behaviour is unchanged

main runs 391 tests, 0 failed; this branch runs 391 tests, 0 failed. The entire mirrored suite passing without a line changed is the evidence that the delegation preserves behaviour.

The mirrored uint256 suite stays exactly as it is. Its behavioural assertions are what would catch a future change to the wrapper — a delegation check would not — and tests are only ever for the future.

forge build --deny warnings, forge fmt --check and forge lint are all clean.

Public surface

Unchanged: same names, parameter types, return types and mutability on every function in both rewritten files. Nothing is added to the surface at all — there are no longer any private functions in either file. The release stays a patch bump.

Audit scope

All four library files are inside the Protofire scope recorded in audit/audits.json. This changes two of them structurally; the bytes32 pair — where the assembly under review lives — is untouched, and its compiled output is byte-identical.

QA

  • Discriminating tests: n/a — no test is added or changed, by design. The existing mirrored suite (391 tests, unchanged) is the discriminating evidence: it passes identically on main and on this branch, and is what a behaviour-changing delegation would break.
  • Mutations applied: 26 mutants over the delegation/relabel layer of the new spelling, run through the adversarial-mutation-test skill and its bundled mutation-probe26/26 killed, 0 survived, 0 no-run, 0 harness errors. Each targets one delegation or one relabel (M04 unsafeAsUint256Array relabel is an offset view not the identity, M18 matrix startPointer delegates to dataPointer, M24 matrixFrom3 drops its third array, M26 flatten returns an empty array, …) and the probe names the tests that killed it. Five of the 26 attack the relabel itself by making it an offset view rather than the identity, which is the failure mode this spelling opens up: 37 relabel assignments across 33 assembly blocks, where the helper spelling had 6 helper bodies.
  • Oracle: the bytes32 implementation being delegated to, plus the deployed-bytecode comparison — Bytes32Harness byte-identical across main and both spellings, so the hot path is proven unmoved rather than argued. The adversarial question this respelling raises is whether the 21 uninitialised memory locals the relabels are assigned into allocate: if any did, unsafeExtend would silently lose its in-place path and no content assertion would notice. Falsified against the existing allocator tests, not argued — LibUint256Array.allocation.t.sol::testExtendInlineAllocatesExactly pins the free memory pointer to the exact end of the extended array and LibUint256Array.extend.t.sol::testExtendInlineEmptyExtendKeepsBasePointer pins the in-place branch, and both pass unchanged.
  • Category check: Nothing detects the uint256 and bytes32 TEST suites drifting apart #128 asks for the mirror to be collapsed; covered for src/. The mirrored test suite stays as it is, so this is Refs #128, not Closes.

`uint256[]` and `bytes32[]` are the same structure in memory, so the two
library pairs were identical modulo the element type and were kept in sync
by hand. Nothing detected them drifting, and they had drifted twice.

`LibBytes32Array`/`LibBytes32Matrix` keep the implementation; the uint256
libraries relabel and delegate. bytes32 is the hot path in this stack and the
opaque-bits type, with uint256 as the numeric view over it, so the wrapper
surcharge lands on the numeric side.

The public surface is unchanged and the whole existing test suite, including
all 14 mirrored bytes32 test files, passes untouched.

Refs #128

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

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3c6e9b50-1ce8-4d2c-9bea-ef47b717bb37

📥 Commits

Reviewing files that changed from the base of the PR and between 333c19e and 832f581.

📒 Files selected for processing (2)
  • src/lib/LibUint256Array.sol
  • src/lib/LibUint256Matrix.sol

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


Walkthrough

LibUint256Array and LibUint256Matrix now reuse bytes32 implementations through zero-copy memory relabelling. Inline assembly for construction, casting, truncation, extension, reversal, counting, and flattening was removed.

Changes

Uint256 delegation

Layer / File(s) Summary
Array relabelling and delegated construction
src/lib/LibUint256Array.sol
Adds uint256/bytes32 relabelling helpers. Pointer access, casting, single-item construction, and arrayFrom overloads delegate to LibBytes32Array.
Array truncation and transformation delegation
src/lib/LibUint256Array.sol
Delegates multi-head construction, truncation, extension, and reversal to LibBytes32Array.
Matrix relabelling and delegated operations
src/lib/LibUint256Matrix.sol
Adds zero-copy relabelling helpers. Pointer access, matrix construction, counting, and flattening delegate to LibBytes32Matrix.

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

Merge Risk: ⚪ Minimal · up to 832f5

The PR replaces the uint256 mirror implementations with private cast wrappers while preserving the public API and tested behavior; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing duplicated uint256 array and matrix implementations with cast wrappers over bytes32 libraries.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-18-issue-128-collapse-mirror

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.

The two `private` cast helpers in each wrapper had a body of `relabelled :=
array` and nothing else, so they bought a call frame to move a pointer between
two types that are the same structure in memory. A jump is worth paying for
real logic, not for a relabel. Each delegation now does its own relabel in an
inline assembly block where it needs one.

Dropping the helpers also drops the two named returns they declared, which are
not this repo's convention (#135).

No public signature, no behaviour and no test changes. All 14 mirrored test
files are still untouched and the whole suite still passes unchanged.

Refs #128

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister
thedavidmeister merged commit 9157dad into main Aug 18, 2026
4 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