Add canonicalize(Float) -> Float for byte-equivalence use cases#237
Add canonicalize(Float) -> Float for byte-equivalence use cases#237thedavidmeister wants to merge 3 commits into
Conversation
Floats are non-canonical by design: multiple (coefficient, exponent) pairs encode the same numeric value and `eq` rescales before comparing rather than relying on byte equality. Consumers that need raw-byte equality (mapping(Float => X) keys, hashing, set membership, content-addressed storage) had no way to obtain a canonical form. Add `LibDecimalFloat.canonicalize` as a public-API (internal) library function. It returns the representative with the largest |coefficient| that fits int224 subject to the exponent staying >= int32.min, reached by scaling the coefficient up by ten directly within those bounds. This avoids the two failure modes of the obvious `packLossless(maximizeFull)` composition (packLossless flagging value-preserving int224 truncation as lossy, and packLossy silently zeroing tiny inputs on exponent underflow). The function never reverts for any valid input Float, is idempotent, and is value-preserving (the result is `eq` to the input). The function is internal and unused by the concrete `DecimalFloat` contract, so it is dead-code-eliminated from the deployed bytecode: `testDeployAddress` and `testExpectedCodeHashDecimalFloat` still pass, so no redeploy / deploy-constant regeneration is required. Adds test/src/lib/LibDecimalFloat.canonicalize.t.sol covering zero, byte-equality across representations (positive and negative), value preservation, idempotence, and three fuzz tests (value preservation, idempotence, eq-implies-byte-equal) at 5096 runs each. Fixes #183 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 11 minutes and 48 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughThis PR adds an internal ChangesFloat Canonicalization
🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
The existing canonicalize suite stays inside the int128/uint8 "easy middle": every coefficient fits int224 with room to spare, every exponent is far from int32.min, and the safety property (numerically distinct Floats must canonicalize to byte-UNEQUAL results) is never asserted at all. Add test/src/lib/LibDecimalFloat.canonicalizeAdversarial.t.sol covering: - NO-COLLISION (the documented "eq iff byte-equal" guarantee in its contrapositive): fuzzed over the full int224 coefficient x int32 exponent box and over a narrow band pinned to the int32.min floor, asserting !eq(a,b) implies the canonical bytes differ; plus concrete floor-pinned and int224-cap cases. Kills a mutant that forces the packed exponent to int32.min (which collapses distinct values). - eq-iff-byte-equal in BOTH directions over raw int224/int32 inputs. - Cross-representation uniqueness at the int32.min floor: the same value reached via a floor-pinned rep and a one-step-higher rep must produce identical bytes. Kills an off-by-one mutant on the `exponent > type(int32).min` floor condition that the existing tests miss. - Value preservation + idempotence pinned to the int32.min floor band (coefficient NOT maximised) and to the int224.min/int224.max coefficient edges. Kills mutants that drop the int224 overflow guard or change the x10 scaling step. All tests pass on the unmutated implementation at 5096 fuzz runs and were mutation-validated against four production mutations (drop the int224 guard, off-by-one floor, x100 scaling step, force-floor exponent); each mutation is killed by at least one new test. No production code change. The scaling/floor structure was additionally proved injective and collision-free by exhaustive enumeration over scaled-down int224/int32 analogues, so no bug was found; these tests pin that down at the real boundaries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ncrete Rework per maintainer review. Requirement 1 — tests organized per source, not a separate "adversarial" file. Moved every test from test/src/lib/LibDecimalFloat.canonicalizeAdversarial.t.sol into the existing source-organized test/src/lib/LibDecimalFloat.canonicalize.t.sol (boundary/no-collision/floor/int224-extreme tests) and deleted the adversarial file. The int224/int32 boundary constants moved with them (dropping the unused INT32_MAX). 19 tests, all passing. Requirement 2 — expose canonicalize in the concrete for deployment + wasm. LibDecimalFloat.canonicalize was internal, so it was dead-code-eliminated from the deployed DecimalFloat contract and unreachable from Rust/JS. Added canonicalize(Float) returns (Float) as an external pure method on src/concrete/DecimalFloat.sol (matching abs/minus). Wired it through the Rust layer: Float::canonicalize in crates/float/src/lib.rs (revm-delegating) and the canonicalize wasm export in crates/float/src/js_api.rs. Added a concrete fuzz test asserting the deployed method matches the lib result, a Rust doctest, and a JS binding test. Regenerated the committed ABI (crates/float/abi/DecimalFloat.json) via CopyArtifacts. Deploy consequence — exposing the method changes DecimalFloat's bytecode, hence its Zoltu address + codehash. Updated the pinned constants in src/lib/deploy/LibDecimalFloatDeploy.sol to the new values: ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = 0x6b7C246F02E67299b5801f8215d7f40abD82056d DECIMAL_FLOAT_CONTRACT_HASH = 0x61bbc303586dc1b233644acdebe38dcc757907d7b39edcf6b1152c2081cf3197 so testDeployAddress / testExpectedCodeHashDecimalFloat pass against the new bytecode. An on-chain redeploy (manual-sol-artifacts.yaml -f suite=decimal-float) is still required to land the new bytecode at this address; that is a separate human-triggered step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #183.
What
Adds
LibDecimalFloat.canonicalize(Float) -> Float: a public-API library function that returns a unique byte representation per numeric value, so two Floats are numerically equal iff their canonical forms are byte-equal. Intended for consumers that need raw-byte equality (mapping(Float => X)keys, hashing, set membership, content-addressed storage).How
Floats are non-canonical by design —
eqrescales before comparing rather than relying on byte equality.canonicalizepicks the representative with the largest|coefficient|that fits int224 subject to the exponent staying>= type(int32).min, reached by the loop-multiply approach from the issue:This avoids the two failure modes of the obvious
packLossless(maximizeFull(...))composition described in the issue (packLosslessflagging value-preserving int224 truncation as lossy;packLossysilently zeroing tiny inputs on exponent underflow). The function never reverts for any valid input Float, is idempotent, and is value-preserving (eqto the input).Bytecode / deploy impact
canonicalizeisinternaland unused by the concreteDecimalFloatcontract, so Solidity dead-code-eliminates it from the deployed bytecode.testDeployAddressandtestExpectedCodeHashDecimalFloatboth still pass — no redeploy or deploy-constant regeneration required.Tests
New
test/src/lib/LibDecimalFloat.canonicalize.t.sol(matches the draft test list in the issue):testCanonicalizeZero— zero at any exponent canonicalizes toFLOAT_ZERO.testCanonicalizeEqualValuesByteEqual/testCanonicalizeNegativeEqualValuesByteEqual— different representations of the same value canonicalize to identical bytes32 (positive and negative).testCanonicalizeValuePreserving/testCanonicalizeIdempotent— concrete value preservation and idempotence.testCanonicalizeValuePreservingFuzz(int224,int32),testCanonicalizeIdempotentFuzz(int224,int32),testCanonicalizeEqImpliesByteEqualFuzz(int128,uint8)— fuzzed, 5096 runs each.All 8 pass. Full
forge test: 459 passed, 5 failed — the 5 failures are all inLibDecimalFloatDeployProd.t.soland are pre-existing*_RPC_URLenvironment failures (live-fork prod-deployment checks); they fail identically on a cleanorigin/maincheckout in this sandbox and are unrelated to this change.forge fmt --checkis clean.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests