2025 08 13 maximize moar#99
Conversation
WalkthroughCore arithmetic in LibDecimalFloatImplementation.add and maximize changed to new scaling/overflow handling; LibDecimalFloat.ceil adjusted to 1e76/-76. Multiple tests updated/added to align with the new normalization boundary, overflow behavior, and unpacking logic; one new characteristicMantissa test added; several test names and expectations updated. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant LibDecimalFloat
participant Impl as LibDecimalFloatImplementation
Caller->>LibDecimalFloat: ceil(signedCoefficient, exponent)
alt signedCoefficient >= 0
LibDecimalFloat->>Impl: add(characteristic, exponent, 1e76, -76)
Impl-->>LibDecimalFloat: packed result
else signedCoefficient < 0
LibDecimalFloat-->>Caller: packLossy(original)
end
LibDecimalFloat-->>Caller: packed result
sequenceDiagram
participant Caller
participant Impl as LibDecimalFloatImplementation
Caller->>Impl: add(aCoeff,aExp, bCoeff,bExp)
Impl->>Impl: align exponents as needed
Impl->>Impl: c = aCoeff + bCoeff
alt overflow detected (same sign a/b, sign(a)==sign(c) fails)
Impl->>Impl: scale down aCoeff,bCoeff by 10
Impl->>Impl: increment exponent by 1
Impl->>Impl: recompute sum
else no overflow
Impl->>Impl: use c
end
Impl-->>Caller: normalized sum
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 9
🔭 Outside diff range comments (1)
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (1)
246-275: Minor ordering nit: assume before computing the sumYou compute expectedSignedCoefficient in unchecked before vm.assume(!willOverflow(...)). It’s harmless since overflow wraps and the test is discarded, but you can save work and risk of misleading local debug logs by assuming first.
- unchecked { - expectedSignedCoefficient = signedCoefficientAMaximized + signedCoefficientBMaximized; - // We aren't testing the overflow case in this test. - vm.assume(!willOverflow(signedCoefficientAMaximized, signedCoefficientBMaximized)); - } + // We aren't testing the overflow case in this test. + vm.assume(!willOverflow(signedCoefficientAMaximized, signedCoefficientBMaximized)); + unchecked { + expectedSignedCoefficient = signedCoefficientAMaximized + signedCoefficientBMaximized; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (10)
.gas-snapshot(3 hunks)src/lib/LibDecimalFloat.sol(1 hunks)src/lib/implementation/LibDecimalFloatImplementation.sol(4 hunks)test/src/lib/LibDecimalFloat.ceil.t.sol(1 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol(5 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol(1 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.log10.t.sol(1 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.maximize.t.sol(3 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.pow10.t.sol(1 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.sub.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#59
File: crates/float/src/lib.rs:233-242
Timestamp: 2025-06-17T10:17:56.205Z
Learning: In the rainlanguage/rain.math.float repository, the maintainer 0xgleb prefers to handle documentation additions and improvements in separate issues rather than inline with feature PRs.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#96
File: test/src/lib/implementation/LibDecimalFloatImplementation.maximize.t.sol:15-29
Timestamp: 2025-08-11T14:32:50.439Z
Learning: In test code for the rain.math.float repository, redundant checks may be intentionally kept for clarity and documentation purposes, even when they could be simplified. The maintainer (thedavidmeister) prefers explicit assertions in test code to make the test's intent clear to future readers, prioritizing readability over conciseness.
📚 Learning: 2025-07-24T04:32:14.171Z
Learnt from: rouzwelt
PR: rainlanguage/rain.math.float#83
File: src/concrete/DecimalFloat.sol:248-251
Timestamp: 2025-07-24T04:32:14.171Z
Learning: In the rainlanguage/rain.math.float project, functions in DecimalFloat.sol that return tuples from LibDecimalFloat calls must unpack the tuple into local variables before returning them (rather than returning directly) to maintain compatibility with Slither static analysis checks.
Applied to files:
test/src/lib/LibDecimalFloat.ceil.t.sol
📚 Learning: 2025-08-11T14:30:48.562Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#96
File: test/src/lib/LibDecimalFloat.ceil.t.sol:43-50
Timestamp: 2025-08-11T14:30:48.562Z
Learning: When reviewing Solidity test files using Forge, verify the actual mutability of helper functions like `bound()` before suggesting changes to function mutability specifiers, as Forge implements many test utilities as pure functions.
Applied to files:
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
- GitHub Check: rainix (ubuntu-latest, test-wasm-build)
- GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
- GitHub Check: rainix (macos-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
🔇 Additional comments (26)
.gas-snapshot (8)
257-257: Regression: Mul packed gas increased noticeablyLibDecimalFloatMulTest:testMulPacked rose from ~7,889 to ~8,837 μ. If the “maximize” changes were intended to improve normalization/packing globally, this local regression might be a trade-off worth calling out or revisiting.
- Confirm whether this is an expected trade-off of the new path.
- If not expected, consider profiling the hot path with/without the recent changes to identify the regression.
296-296: Parsing paths got cheaper — nice winBoth ParseLiteralDecimalFloatFuzz and ParsePacked show noticeable gas drops, which is a meaningful improvement on a hot utility.
Also applies to: 304-304
40-40: Consistent improvements in ceil/frac non-revert checksThe μ values decreased across these non-revert guard tests. Looks good and consistent with general optimization.
Also applies to: 93-93, 101-101
205-206: Revert reason matches OverflowRescaleRevert semanticsThe
testWithTargetExponentLargerExponentOverflowRescaleRevertalready usesvm.expectRevert( abi.encodeWithSelector(WithTargetExponentOverflow.selector, signedCoefficient, exponent, targetExponent) );which aligns perfectly with the “OverflowRescaleRevert” name. No changes required.
215-215: Document rationale for Log10/Pow10 gas improvementsThe PR introduces significant gas savings across multiple Log10 and Pow10 benchmarks (e.g.,
testLog10PackedandtestPow10Packed), but the PR description currently doesn’t explain what implementation changes produced these deltas. Please update the PR summary or add a note in the docs to cover:
- Which lookup‐table tweaks, interpolation/rescale adjustments, or other algorithmic changes were made
- How each change maps to the observed gas reductions
Affected snapshot entries (see
.gas-snapshot):
- Lines 173–176
- Line 215 (
LibDecimalFloatLog10Test:testLog10Packed(bytes32))- Line 259
- Lines 265–266
If it helps, I can draft a short PR summary of these deltas for you to refine.
130-146: Additive path looks stable; please confirm overflow-scenario coverageThe gas usage for additive operations is overall stable or improved. I see dedicated tests for min/max values under
test/src/lib/implementation/LibDecimalFloatImplementation.*.t.sol(e.g.min.t.sol,max.t.sol,add.t.sol,maximize.t.sol), but I didn’t spot an explicit overflow-check test. Before merging, please:• Verify that
LibDecimalFloatImplementation.addstill exercises both symmetric and asymmetric mantissa/exponent edge cases that could overflow.
• Ensure there’s at least one test asserting that addition of extreme values correctly reverts on overflow.
156-156: Clarify intentional fuzz.runs override in test fileThe low run count (~122) is expected because the test file itself overrides the default fuzz runs:
• test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol:96
/// forge-config: default.fuzz.runs = 100This directive caps the fuzz inputs at 100 (plus ~22 shrink runs), rather than the global
foundry.tomlsetting of 5096.
If you’d prefer this test to use the full default coverage, remove or adjust thatforge-configcomment. Otherwise, no further changes are required.
204-209: No duplicate WithTargetExponent tests foundSearches for
LibDecimalFloatWithTargetExponentTestreturned no hits, and all sixtestWithTargetExponent*functions are defined only in
test/src/lib/implementation/LibDecimalFloatImplementation.withTargetExponent.t.sol. No duplicate coverage remains.src/lib/implementation/LibDecimalFloatImplementation.sol (3)
309-325: Overflow-aware addition logic looks correctThe sign-based overflow detection and single-step downscale by 10 before retrying the add is sound and matches the updated test suite.
547-547: Primary 1e75 maximization gate: sensible shiftUsing 1e75 as the main gate and then attempting a safe push to 1e76 improves tightness of maximization without risking overflow.
583-585: Underflow detection via wrap-around guard is correctThe post-adjustment check against the initial exponent correctly catches signed underflow (wrap to large positive). The revert data also surfaces the wrapped state for easier debugging.
test/src/lib/implementation/LibDecimalFloatImplementation.maximize.t.sol (4)
20-22: isMaximized: boundary widened to include >= 1e76The adjusted check now treats 1e76 and above as maximized. This aligns with the new maximization path that opportunistically pushes to 1e76.
32-32: Renamed test to reflect broader invarianttestMaximizedEverything better communicates that any normalized input should end up maximized post-call.
54-66: Example expectations updated to 1e76 boundary: LGTMThe examples now correctly reflect the new representation. The descending loop bound i - 76 is consistent with the updated scale.
68-70: Edge-case coverage for fuzz-suspect value is a good additionIncluding 54304950862250382, -16 hits a tricky region and helps guard against regressions around the -76 boundary.
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (5)
13-23: Reference overflow predicate: clear and correctThe sign-based predicate matches standard two’s-complement addition overflow rules. Good to have as a readable counterpart to the Yul version.
25-35: Assembly overflow predicate matches implementationThis mirrors the in-contract logic, ensuring the test suite validates the exact branch behavior.
94-101: Addition of ones under new maximization is correctWith both operands maximized to 1e76 at exponent -76, the result 2e76 at -76 is consistent and respects the widened boundary.
116-118: Updated mixed-exponent expectation is consistentThe expected 1.23456789987654321e76 at exponent -59 aligns with the new scaling behavior.
170-244: Expanded “small-to-large” examples correctly reflect precision loss and new limitsThis block thoroughly exercises alignment loss, ordering invariants, and near-boundary behavior (including negatives). Looks consistent with the library’s lossy alignment semantics and the 1e76 cap.
test/src/lib/implementation/LibDecimalFloatImplementation.sub.t.sol (1)
65-65: Updated “max - 1” expectation aligns with new scalingThe shift from 1e8 to 1e9 and exponent decrement is consistent with the wider maximization range used across the library.
test/src/lib/implementation/LibDecimalFloatImplementation.log10.t.sol (2)
34-43: Log10 expected pairs adjusted to -76: consistent with 1e76 boundaryThe mantissa staying constant while the exponent increases by 1 matches the new representation rules. Console gas logs remain intact for visibility.
46-47: Interpolated lookup expectation shift is correctThe -76 exponent on the interpolated case is aligned with the global exponent boundary changes.
src/lib/LibDecimalFloat.sol (1)
646-648: Ceil: switch to adding 1 via (1e76, -76) is correct and consistent with new normalization boundaryAdding (1e76, -76) represents +1 exactly and aligns with the 1e76-based normalization used elsewhere in the PR. Logic for mantissa <= 0 remains correct for ceil semantics.
test/src/lib/LibDecimalFloat.ceil.t.sol (2)
22-23: Good fix: don’t shadow/mutate inputs when unpacking resultsBinding to local actualSignedCoefficient/actualExponent avoids accidental parameter mutation and makes the intent explicit.
25-29: Robust equality check and targeted logging on failureUsing LibDecimalFloatImplementation.eq to compare the result against expected is appropriate here. The additional logs for actual values gated by the failure branch improve diagnosis without cluttering happy-path output.
Also applies to: 34-35
| // Maybe we can fit in one more OOM without overflow, but we won't | ||
| // know until we try. This pushes us into [1e76,type(int256).max] and | ||
| // [-type(int256).max,-1e76] ranges, if that's possible. | ||
| int256 trySignedCoefficient = signedCoefficient * 10; | ||
| if (signedCoefficient == trySignedCoefficient / 10) { | ||
| signedCoefficient = trySignedCoefficient; | ||
| exponent -= 1; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Use bound check instead of mul-then-div for overflow probe (micro-gas + clarity)
The current try-multiply-then-divide round-trip is correct but slightly opaque and does an unnecessary multiply when it will overflow. A clearer and cheaper pattern is to pre-check bounds.
Apply this refactor:
- int256 trySignedCoefficient = signedCoefficient * 10;
- if (signedCoefficient == trySignedCoefficient / 10) {
- signedCoefficient = trySignedCoefficient;
- exponent -= 1;
- }
+ // Safe to multiply by 10 without overflow?
+ if (
+ signedCoefficient <= type(int256).max / 10 &&
+ signedCoefficient >= type(int256).min / 10
+ ) {
+ signedCoefficient *= 10;
+ exponent -= 1;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Maybe we can fit in one more OOM without overflow, but we won't | |
| // know until we try. This pushes us into [1e76,type(int256).max] and | |
| // [-type(int256).max,-1e76] ranges, if that's possible. | |
| int256 trySignedCoefficient = signedCoefficient * 10; | |
| if (signedCoefficient == trySignedCoefficient / 10) { | |
| signedCoefficient = trySignedCoefficient; | |
| exponent -= 1; | |
| } | |
| // Maybe we can fit in one more OOM without overflow, but we won't | |
| // know until we try. This pushes us into [1e76,type(int256).max] and | |
| // [-type(int256).max,-1e76] ranges, if that's possible. | |
| // Safe to multiply by 10 without overflow? | |
| if ( | |
| signedCoefficient <= type(int256).max / 10 && | |
| signedCoefficient >= type(int256).min / 10 | |
| ) { | |
| signedCoefficient *= 10; | |
| exponent -= 1; | |
| } |
🤖 Prompt for AI Agents
In src/lib/implementation/LibDecimalFloatImplementation.sol around lines 574 to
581, replace the mul-then-div overflow probe with a pre-check against int256
bounds: test that signedCoefficient <= type(int256).max / 10 and
signedCoefficient >= type(int256).min / 10 before doing signedCoefficient *= 10
and exponent -= 1; this avoids the unnecessary multiply that would overflow and
is clearer and cheaper gas-wise.
There was a problem hiding this comment.
i doubt it is cheaper as && involves a jump in solidity
There was a problem hiding this comment.
@thedavidmeister You're absolutely right about the && operator involving jumps in Solidity due to short-circuit evaluation. The current mul-then-div approach is likely more gas-efficient since it avoids the conditional jump overhead, especially when overflow is rare. I appreciate the correction on the gas optimization nuances!
🐰
ears perk up with newfound knowledge about Solidity gas optimization
✏️ Learnings added
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#99
File: src/lib/implementation/LibDecimalFloatImplementation.sol:574-581
Timestamp: 2025-08-14T16:56:28.956Z
Learning: In the rainlanguage/rain.math.float repository, maintainer thedavidmeister avoids using `&&` operators in gas-critical paths because they involve jumps in Solidity due to short-circuit evaluation, preferring approaches like mul-then-div overflow probes that avoid conditional jump overhead.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#99
File: src/lib/implementation/LibDecimalFloatImplementation.sol:309-325
Timestamp: 2025-08-14T16:32:05.897Z
Learning: In the rainlanguage/rain.math.float repository, maintainer thedavidmeister prefers to keep assembly-based overflow checks inline for gas optimization rather than extracting them into helper functions, even when it results in code duplication.
| // and the mantissa is the whole coefficient. | ||
| if (exponent < -76) { |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Comment fix improves clarity
Updating the note to “mantissa is the whole coefficient” precisely matches the implementation returning (0, signedCoefficient) for exponent < -76.
If you want, mirror this clarification in the function’s NatSpec for consistency.
🤖 Prompt for AI Agents
In src/lib/implementation/LibDecimalFloatImplementation.sol around lines 818 to
819, update the inline comment to read "and the mantissa is the whole
coefficient." to match the implementation branch that returns (0,
signedCoefficient) when exponent < -76; also mirror this clarification in the
function NatSpec comment so the documentation matches the code behavior.
| else if (mantissa > 0) { | ||
| (characteristic, exponent) = LibDecimalFloatImplementation.add(characteristic, exponent, 1e75, -75); | ||
| (characteristic, exponent) = LibDecimalFloatImplementation.add(characteristic, exponent, 1e76, -76); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Avoid magic numbers: centralize the unpacked “one” as a named constant/helper
The pair (1e76, -76) is a magic number here and appears in tests as well. Consider introducing a single source of truth for the unpacked “one” to improve readability and reduce future churn if the normalization boundary shifts again.
Example approach (outside this file, in LibDecimalFloatImplementation.sol or a shared constants file):
// In LibDecimalFloatImplementation.sol
int256 constant ONE_COEFFICIENT = 1e76;
int256 constant ONE_EXPONENT = -76;Then here:
- (characteristic, exponent) = LibDecimalFloatImplementation.add(characteristic, exponent, 1e76, -76);
+ (characteristic, exponent) = LibDecimalFloatImplementation.add(
+ characteristic,
+ exponent,
+ LibDecimalFloatImplementation.ONE_COEFFICIENT,
+ LibDecimalFloatImplementation.ONE_EXPONENT
+ );🤖 Prompt for AI Agents
In src/lib/LibDecimalFloat.sol around lines 646 to 648, the unpacked “one” is
used as a magic pair (1e76, -76); introduce a named constant (or two constants)
in LibDecimalFloatImplementation.sol or a shared constants file (e.g.,
ONE_COEFFICIENT = 1e76 and ONE_EXPONENT = -76 or a single struct/tuple helper)
and replace the literal pair here with those constants; update any tests that
use the same literals to reference the centralized constants so the
normalization boundary has a single source of truth.
| checkPow10(1.55555e37, -37, 35.935e75, -75); | ||
| // 10^1234.56789 | ||
| checkPow10(123456789, -5, 36979e71, 1159); | ||
| checkPow10(123456789, -5, 3.6979e76, 1158); | ||
| // ~= 10 (fuzzing found this edge case). |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Updated expectations look right under the 1e76 normalization; add a numeric-invariant to reduce brittleness
The new expected pairs reflect the 1e76 boundary normalization and preserve numeric equivalence. To future-proof against representation-only shifts, consider also asserting numeric equality using pack + eq in checkPow10 (in addition to the explicit pair checks).
You can enhance checkPow10 as follows (outside this hunk):
function checkPow10(
int256 signedCoefficient,
int256 exponent,
int256 expectedSignedCoefficient,
int256 expectedExponent
) internal {
address tables = logTables();
(int256 actualSignedCoefficient, int256 actualExponent) =
LibDecimalFloatImplementation.pow10(tables, signedCoefficient, exponent);
assertEq(actualSignedCoefficient, expectedSignedCoefficient, "signedCoefficient");
assertEq(actualExponent, expectedExponent, "exponent");
// Numeric invariant (representation-agnostic)
Float actual = LibDecimalFloat.packLossless(actualSignedCoefficient, actualExponent);
Float expected = LibDecimalFloat.packLossless(expectedSignedCoefficient, expectedExponent);
assertTrue(actual.eq(expected), "numeric mismatch despite representation");
}This keeps the explicit pair checks (as preferred in this repo’s tests) and augments them with a numeric invariant.
🤖 Prompt for AI Agents
In test/src/lib/implementation/LibDecimalFloatImplementation.pow10.t.sol around
lines 64 to 67, the new expected pairs are fine but the test is brittle to
representation-only shifts; update the checkPow10 helper (outside this hunk) to
keep the explicit assertEq checks and add a representation-agnostic numeric
invariant: call LibDecimalFloat.packLossless on the actual and expected
signedCoefficient/exponent pairs and assert their equality with .eq (assertTrue)
to ensure numeric equivalence even if packing/normalization changes.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…in.math.float into 2025-08-13-maximize-moar
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
.gas-snapshot (2)
149-154: Typo fix confirmed: testOverflowChecks rename reflected in snapshotThe snapshot now correctly references testOverflowChecks. Thanks for cleaning this up.
182-185: Large gas deltas in Log10/Pow10 paths—add a short note to PR (follow-up issue OK)Substantial improvements are visible (e.g., testLog10Packed μ dropping to ~1.27M, Pow10 interpolated lookups changing). A brief note in the PR about why these changed (e.g., knock-on effects of maximize/exponent handling) will aid reviewers.
Per repo preference (0xgleb), it’s fine to track this as a separate docs issue and link it from the PR.
Also applies to: 205-209, 224-224
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (1)
.gas-snapshot(3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-11T14:30:48.562Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#96
File: test/src/lib/LibDecimalFloat.ceil.t.sol:43-50
Timestamp: 2025-08-11T14:30:48.562Z
Learning: When reviewing Solidity test files using Forge, verify the actual mutability of helper functions like `bound()` before suggesting changes to function mutability specifiers, as Forge implements many test utilities as pure functions.
Applied to files:
.gas-snapshot
📚 Learning: 2025-06-16T13:17:28.513Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: src/concrete/DecimalFloat.sol:175-182
Timestamp: 2025-06-16T13:17:28.513Z
Learning: In the rainlanguage/rain.math.float codebase, there's an established naming convention where functions accepting a `Float` type parameter consistently use `float` as the parameter name, even though it shadows the type name. This pattern is used throughout `LibDecimalFloat.sol` and should be maintained for consistency in related contracts like `DecimalFloat.sol`.
Applied to files:
.gas-snapshot
📚 Learning: 2025-06-17T10:17:56.205Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#59
File: crates/float/src/lib.rs:233-242
Timestamp: 2025-06-17T10:17:56.205Z
Learning: In the rainlanguage/rain.math.float repository, the maintainer 0xgleb prefers to handle documentation additions and improvements in separate issues rather than inline with feature PRs.
Applied to files:
.gas-snapshot
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: git-clean
- GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, test-wasm-build)
- GitHub Check: rainix (macos-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
🔇 Additional comments (2)
.gas-snapshot (2)
213-218: Old WithTargetExponent tests removed – no duplicates foundI’ve verified there are no remaining files or references to
LibDecimalFloatWithTargetExponentTest. Only the newLibDecimalFloatImplementationWithTargetExponentTestexists intest/src/lib/implementation/LibDecimalFloatImplementation.withTargetExponent.t.sol.
36-41: Ceil tests already cover the new 1e76 boundary cases
- testCeilInRange bounds
exponentat –76…–1, so the “exactly 1e76” (exponent = –76) path is exercised (with both x>0 and x≤0).- testCeilLessThanMin bounds at ≤–77, covering the “below 1e76” branch.
- testCeilNonNegative and testCeilZero cover the non-negative and zero cases.
- packLossless always normalizes coefficients into the <1e76 range before ceil, so there’s no untested path at coefficient=1e76 or 1e76–1.
No additional ceil boundary tests are required.
| DecimalFloatMaxTest:testMaxDeployed(bytes32,bytes32) (runs: 5118, μ: 2586274, ~: 2586213) | ||
| DecimalFloatMinTest:testMinDeployed(bytes32,bytes32) (runs: 5118, μ: 2586295, ~: 2586233) | ||
| DecimalFloatMinusTest:testMinusDeployed(bytes32) (runs: 5118, μ: 2586009, ~: 2586009) | ||
| DecimalFloatMulTest:testMulDeployed(bytes32,bytes32) (runs: 5118, μ: 2588107, ~: 2589169) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Multiplication gas regressions—confirm expected tradeoff or consider micro-opts
Notable increases:
- DecimalFloatMulTest:testMulDeployed μ: 2,588,107 -> 2,589,169
- LibDecimalFloatMulTest:testMulPacked μ: 7,889 -> 8,837
If expected due to safer overflow checks/extra normalization, please note in the PR. Otherwise, consider:
- Avoiding redundant maximize/withTargetExponent passes in the mul path.
- Fast-path when both operands share exponent and the product fits without rescale.
- Hoisting/common-subexpressions for any repeated willOverflow-style checks.
Happy to deep-dive the mul implementation and propose targeted diffs if you share the relevant snippets.
Also applies to: 266-266
🏁 Script executed:
#!/bin/bash
git diff HEAD~1 -- .gas-snapshotLength of output: 1245
🏁 Script executed:
#!/bin/bash
grep -n "DecimalFloatMulTest" .gas-snapshotLength of output: 225
.gas-snapshot has incorrect snippet and multiplication gas regressions detected
• Corrupted snapshot for LibDecimalFloatImplementationAddTest:testOverflowChecks (lines 142–150 in .gas-snapshot): a code snippet was inserted instead of the gas-measurement line. Please restore it to something like:
LibDecimalFloatImplementationAddTest:testOverflowChecks(int256,int256) (runs: 5118, μ: 3837, ~: 3823)
• Notable multiplication regressions:
– Line 25: DecimalFloatMulTest:testMulDeployed μ: 2,588,107 → 2,589,169 (+1,062)
– Line 266: LibDecimalFloatMulTest:testMulPacked μ: 7,889 → 8,837 (+948)
If these increases are expected (e.g. due to safer overflow checks or extra normalization), please note the trade-off in the PR description. Otherwise, consider micro-optimizations in the mul path:
• Eliminate redundant maximize/withTargetExponent passes
• Add a fast-path when both operands share an exponent and no rescale is needed
• Hoist common sub-expressions for repeated overflow checks (willOverflow)
🤖 Prompt for AI Agents
.gas-snapshot around line 25 (and corrupt snippet at lines 142–150): restore the
corrupted gas-measurement line for
LibDecimalFloatImplementationAddTest:testOverflowChecks to the proper format
(e.g. "LibDecimalFloatImplementationAddTest:testOverflowChecks(int256,int256)
(runs: 5118, μ: 3837, ~: 3823)") and update line 25 and line 266 entries if
their new μ values are correct; if the increases are expected, add a brief note
in the PR description explaining the trade-off (safer overflow
checks/normalization), otherwise inspect the mul path and apply
micro-optimizations: remove redundant maximize/withTargetExponent passes, add a
fast-path when exponents match and no rescale is required, and hoist repeated
willOverflow sub-expressions to reduce gas.
| LibDecimalFloatImplementationAddTest:testAdd123456789e9987654321() (gas: 4795) | ||
| LibDecimalFloatImplementationAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 5118, μ: 13126, ~: 13053) | ||
| LibDecimalFloatImplementationAddTest:testAddOneOneNotMaximized() (gas: 6148) | ||
| LibDecimalFloatImplementationAddTest:testAddOneOnePreMaximized() (gas: 4235) | ||
| LibDecimalFloatImplementationAddTest:testAddOneZero() (gas: 3688) | ||
| LibDecimalFloatImplementationAddTest:testAddSameExponent(int256,int256) (runs: 5112, μ: 6890, ~: 6957) | ||
| LibDecimalFloatImplementationAddTest:testAddZero() (gas: 3687) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroAnyExponent(int128) (runs: 5109, μ: 9294, ~: 9271) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroOne() (gas: 3664) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 5108, μ: 13985, ~: 13961) | ||
| LibDecimalFloatImplementationAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 71367) | ||
| LibDecimalFloatImplementationAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 5099, μ: 16839, ~: 16821) | ||
| LibDecimalFloatImplementationAddTest:testGasAddOne() (gas: 1179) | ||
| LibDecimalFloatImplementationAddTest:testGasAddZero() (gas: 360) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroAnyExponent(int128) (runs: 5118, μ: 9292, ~: 9271) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroOne() (gas: 3686) | ||
| LibDecimalFloatImplementationAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 5117, μ: 13996, ~: 13970) | ||
| LibDecimalFloatImplementationAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 92108) | ||
| LibDecimalFloatImplementationAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 5101, μ: 17007, ~: 16985) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Addition now needs explicit overflow-handling tests (positive and negative cases)
Given maximize can now enable addition overflow, please add tests that:
- Assert the chosen behavior when |coeff_a| + |coeff_b| exceeds the signed 224-bit limit after alignment (revert vs. rescale vs. other).
- Mirror tests for negative sums and mixed-sign near-canceling values.
- Cover “already-maximized” operands vs. “not-maximized” to ensure consistent semantics.
To quickly verify if such tests already exist:
🏁 Script executed:
#!/bin/bash
# Look for revert expectations and overflow mentions near add() in tests.
rg -n -A2 -B4 'expect(Revert| revert)|expectRevert' test | rg -n -A2 -B2 -i 'add\('
rg -n -i 'overflow.*add|add.*overflow'
# Check the add test files for any explicit revert assertions.
fd -a add.t.sol test | xargs -I{} sh -c 'echo "--- {} ---"; rg -n -A2 -B2 -i "revert|overflow" "{}"'Length of output: 3185
Add explicit overflow-handling tests for add
The existing fuzz‐and‐catch patterns (e.g. in DecimalFloat.add.t.sol and LibDecimalFloatImplementation.add.t.sol) only verify that a revert occurs—but they neither drive the code into overflow nor assert the exact behavior (revert message, rescale fallback, etc.). We need targeted test cases that exercise coefficient overflow under maximize:
• In test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (within LibDecimalFloatImplementationAddTest):
– Positive‐overflow: choose a, b so |a| + |b| > 2^223 and assert the implementation’s chosen behavior (e.g. specific revert message or rescale).
– Negative‐overflow: sums below –2^223.
– Mixed‐sign near‐canceling: a ≈ –b but aligned coefficients still overflow.
– Already‐maximized vs. not‐maximized inputs to confirm consistency.
• In test/src/concrete/DecimalFloat.add.t.sol: mirror the above scenarios against the high‐level DecimalFloat wrapper, asserting identical overflow semantics (message/code).
Once added, these tests will guarantee that coefficient overflow is handled (or properly signaled) in all edge cases.
| LibDecimalFloatImplementationEqTest:testEqNotReverts(int256,int256,int256,int256) (runs: 5118, μ: 654, ~: 679) | ||
| LibDecimalFloatImplementationEqTest:testEqOneEAny(int256,int256) (runs: 5118, μ: 3416, ~: 3416) | ||
| LibDecimalFloatImplementationEqTest:testEqReference(int256,int256,int256,int256) (runs: 5118, μ: 9903, ~: 11446) | ||
| LibDecimalFloatImplementationEqTest:testEqX(int256) (runs: 5118, μ: 3392, ~: 3392) |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
❓ Verification inconclusive
Equality reference test gas increase—ensure compare path avoids add/overflow checks
μ increased 9,903 -> 11,446. If the equality path now incidentally routes through add/overflow detection or non-essential normalization, consider:
- Comparing normalized exponents/coefficients directly.
- Avoiding maximize unless strictly necessary for equality.
- Using short-circuit checks for sign/exponent mismatches.
If this is an intentional tradeoff, a one-liner in the PR would help.
🏁 Script executed:
#!/bin/bash
# Locate the equality implementation in Solidity files
rg "function eq" -n . --include '*.sol'Length of output: 483
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Locating EqTest contract ==="
rg -g '*.sol' -n 'contract LibDecimalFloatImplementationEqTest' .
echo
echo "=== Locating Implementation library ==="
rg -g '*.sol' -n 'library LibDecimalFloatImplementation' .
echo
echo "=== Searching for `eq` function definitions ==="
rg -g '*.sol' -n 'function eq' .Length of output: 1816
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== eq in Implementation Library ==="
rg -n -C 10 'function eq\(int256 signedCoefficientA' src/lib/implementation/LibDecimalFloatImplementation.sol
echo
echo "=== eq in Base Library ==="
rg -n -C 10 'function eq\(Float a, Float b\)' src/lib/LibDecimalFloat.solLength of output: 2529
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== compareRescale definition ==="
rg -n -C 5 'function compareRescale' src/lib/implementation/LibDecimalFloatImplementation.sol
echo
echo "=== multiplyByExp definition ==="
rg -n -C 5 'function multiplyByExp' src/lib/implementation/LibDecimalFloatImplementation.solLength of output: 885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== compareRescale (lines 690–760) ==="
sed -n '690,760p' src/lib/implementation/LibDecimalFloatImplementation.sol
echo
echo "=== multiplyByExp in base library ==="
rg -n -C 20 'function multiplyByExp' src/lib/LibDecimalFloat.sol
echo
echo "=== LibDecimalFloatImplementationEqTest (first 200 lines) ==="
sed -n '1,200p' test/src/lib/implementation/LibDecimalFloatImplementation.eq.t.solLength of output: 3142
Equality reference test gas increase—please verify normalization path
The mean gas cost for LibDecimalFloatImplementationEqTest:testEqReference rose from 9 903 to 11 446 (≈15 %). It appears the eq implementation now always calls through compareRescale, performing assembly‐based overflow checks and 10**exponentDiff even when exponents match. Before accepting this regression, please:
- Confirm that when exponents are equal (or one coefficient is zero or signs differ),
compareRescaletruly short-circuits before theEXPand multiply overflow logic. - If unavoidable, document this trade-off in the PR (e.g. a one-line note calling out the added normalization cost).
- Otherwise, consider early exits via simple sign/exponent comparisons to skip the
10**and overflow checks.
[src/lib/implementation/LibDecimalFloatImplementation.sol: eq + compareRescale]
🤖 Prompt for AI Agents
In src/lib/implementation/LibDecimalFloatImplementation.sol around the eq and
compareRescale functions (referenced at .gas-snapshot line 173), verify and
adjust the normalization path so equal-exponent or trivial cases avoid the
expensive EXP and multiply-overflow assembly work: add explicit early-return
checks for exponent equality, zero coefficients, or differing signs before
calling compareRescale; if bypassing the EXP/overflow logic is impossible, add a
one-line PR comment in the function docstring or change log noting the added
normalization gas cost; ensure the code path that currently performs
10**exponentDiff and assembly overflow checks is only reached when necessary.
| LibDecimalFloatImplementationMaximizeTest:testMaximizedExamples() (gas: 165819) | ||
| LibDecimalFloatImplementationMaximizeTest:testMaximizedIdempotent(int256,int256) (runs: 5118, μ: 9903, ~: 9868) | ||
| LibDecimalFloatImplementationMinusTest:testMinusIsSubZero(int256,int256,int256) (runs: 5118, μ: 12950, ~: 12929) | ||
| LibDecimalFloatImplementationMulTest:testMul123456789987654321() (gas: 3676) |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
🛠️ Refactor suggestion
Maximize tests look good; add boundary cases for exponent underflow and MSB shift
To fully exercise the new maximize strategy, please add/confirm tests for:
- Mantissa that can be left-shifted exactly into the MSB without overflow (positive and negative).
- Minimal exponent edge where subtracting 1 would underflow; assert behavior (revert vs. rescale vs. no-op).
- Values around 1e75..1e76 and ~5.78e76 boundaries to ensure no off-by-one behavior.
- Idempotency specifically at extreme bounds (min exponent, near-max mantissa).
I can draft these test cases if helpful.
🤖 Prompt for AI Agents
In .gas-snapshot around lines 186-189, the maximize test coverage is missing
boundary cases for exponent underflow and MSB shifting; add unit tests that (1)
exercise mantissas that can be left-shifted exactly into the MSB (both positive
and negative values) and assert no overflow and correct sign/scale, (2) cover
the minimal exponent edge where decrementing the exponent would underflow and
assert the expected behavior (explicitly test for revert, rescale, or no-op
depending on the contract spec), (3) test values around 1e75–1e76 and the
~5.78e76 boundaries to catch off-by-one errors, and (4) add idempotency checks
at extreme bounds (min exponent and near-max mantissa) to ensure repeated
maximize calls are no-ops; for each test include precise input values, expected
outcomes (reverted or resulting normalized value and exponent), and gas
assertions if relevant.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol (1)
9-20: Solid helper + assertions; negative-path coverage addedThe helper is minimal and pure, and the negative signedCoefficient example in the examples test addresses earlier feedback about negative behavior.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (1)
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T14:30:48.562Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#96
File: test/src/lib/LibDecimalFloat.ceil.t.sol:43-50
Timestamp: 2025-08-11T14:30:48.562Z
Learning: When reviewing Solidity test files using Forge, verify the actual mutability of helper functions like `bound()` before suggesting changes to function mutability specifiers, as Forge implements many test utilities as pure functions.
Applied to files:
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, test-wasm-build)
- GitHub Check: rainix (macos-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
| function testCharacteristicMantissaExamples() public pure { | ||
| checkCharacteristicMantissa(0, 0, 0, 0); | ||
| checkCharacteristicMantissa(0, 1, 0, 0); | ||
|
|
||
| checkCharacteristicMantissa(5.4304950862250382e16, -16, 5e16, 4304950862250382); | ||
| checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -0.4304950862250382e16); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Add a fuzz invariant test over exponent ∈ [-77, 0]
Property tests will catch regressions without hand-computing expected values. This stays pure by using forge-std’s pure bound().
Add this function to the contract:
function testFuzzCharacteristicMantissaInvariants(int256 signedCoefficient, int256 exponent) public pure {
// Limit to exponents where the scale is well-defined and safe to compute
exponent = bound(exponent, -77, 0);
(int256 c, int256 m) = LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent);
if (exponent == -77) {
// scale = 1e77 > int256.max, so characteristic must be 0 and mantissa equals coefficient
assertEq(c, 0, "Characteristic at -77 must be 0");
assertEq(m, signedCoefficient, "Mantissa at -77 must equal coefficient");
} else {
// For -76..0 we can safely compute scale as int256.
uint256 scaleU = 10 ** uint256(uint256(-exponent)); // <= 1e76 fits int256
int256 scale = int256(scaleU);
// Decomposition invariant
assertEq(c + m, signedCoefficient, "Decomposition invariant");
// Characteristic is a multiple of scale
assertEq(c % scale, 0, "Characteristic must be multiple of scale");
// Mantissa magnitude bound: -scale < m < scale
assertTrue(m > -scale && m < scale, "Mantissa magnitude bound");
// Mantissa sign follows coefficient when non-zero
if (m != 0) {
assertEq(m > 0, signedCoefficient > 0, "Mantissa sign follows coefficient");
}
}
}🤖 Prompt for AI Agents
In
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol
around lines 22 to 28, add a new pure fuzz test that bounds the exponent to
[-77,0] and verifies invariant properties of characteristicMantissa: call
LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient,
exponent) then for exponent == -77 assert characteristic == 0 and mantissa ==
signedCoefficient; otherwise compute scale = int256(10 ** uint256(-exponent))
and assert (1) decomposition c + m == signedCoefficient, (2) c is a multiple of
scale (c % scale == 0), (3) mantissa magnitude -scale < m < scale, and (4) if m
!= 0 then sign(m) == sign(signedCoefficient); use forge-std’s bound() to
constrain exponent and keep the function pure.
🛠️ Refactor suggestion
Add boundary coverage: exact multiples, off-by-one, and -76/-77 exponent edge semantics
These add high-value checks for:
- exact multiple → zero mantissa,
- off-by-one around the multiple,
- exponent -76 boundary (scale = 1e76),
- exponent -77 where scale exceeds int256.max and characteristic must be zero.
This hardens behavior around documented boundaries and negative remainders.
Apply this diff within the examples function:
function testCharacteristicMantissaExamples() public pure {
checkCharacteristicMantissa(0, 0, 0, 0);
checkCharacteristicMantissa(0, 1, 0, 0);
checkCharacteristicMantissa(5.4304950862250382e16, -16, 5e16, 4304950862250382);
- checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -0.4304950862250382e16);
+ checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -4304950862250382);
+
+ // Exact multiple => zero mantissa
+ checkCharacteristicMantissa(5e16, -16, 5e16, 0);
+ checkCharacteristicMantissa(-5e16, -16, -5e16, 0);
+ // Off-by-one around the multiple
+ checkCharacteristicMantissa(5e16 + 1, -16, 5e16, 1);
+ checkCharacteristicMantissa(-5e16 - 1, -16, -5e16, -1);
+
+ // Boundary at exponent -76 (scale = 1e76)
+ checkCharacteristicMantissa(1e76, -76, 1e76, 0);
+ checkCharacteristicMantissa(1e76 + 1, -76, 1e76, 1);
+ checkCharacteristicMantissa(-1e76, -76, -1e76, 0);
+ checkCharacteristicMantissa(-1e76 - 1, -76, -1e76, -1);
+
+ // Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0
+ checkCharacteristicMantissa(1, -77, 0, 1);
+ checkCharacteristicMantissa(-1, -77, 0, -1);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function testCharacteristicMantissaExamples() public pure { | |
| checkCharacteristicMantissa(0, 0, 0, 0); | |
| checkCharacteristicMantissa(0, 1, 0, 0); | |
| checkCharacteristicMantissa(5.4304950862250382e16, -16, 5e16, 4304950862250382); | |
| checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -0.4304950862250382e16); | |
| } | |
| function testCharacteristicMantissaExamples() public pure { | |
| checkCharacteristicMantissa(0, 0, 0, 0); | |
| checkCharacteristicMantissa(0, 1, 0, 0); | |
| checkCharacteristicMantissa(5.4304950862250382e16, -16, 5e16, 4304950862250382); | |
| checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -4304950862250382); | |
| // Exact multiple => zero mantissa | |
| checkCharacteristicMantissa(5e16, -16, 5e16, 0); | |
| checkCharacteristicMantissa(-5e16, -16, -5e16, 0); | |
| // Off-by-one around the multiple | |
| checkCharacteristicMantissa(5e16 + 1, -16, 5e16, 1); | |
| checkCharacteristicMantissa(-5e16 - 1, -16, -5e16, -1); | |
| // Boundary at exponent -76 (scale = 1e76) | |
| checkCharacteristicMantissa(1e76, -76, 1e76, 0); | |
| checkCharacteristicMantissa(1e76 + 1, -76, 1e76, 1); | |
| checkCharacteristicMantissa(-1e76, -76, -1e76, 0); | |
| checkCharacteristicMantissa(-1e76 - 1, -76, -1e76, -1); | |
| // Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0 | |
| checkCharacteristicMantissa(1, -77, 0, 1); | |
| checkCharacteristicMantissa(-1, -77, 0, -1); | |
| } |
…t.sol Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…in.math.float into 2025-08-13-maximize-moar
There was a problem hiding this comment.
Actionable comments posted: 2
🔭 Outside diff range comments (1)
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (1)
108-110: Incorrect coefficient in test assertionAfter summing 123456789 + 987654321 = 1111111110 and normalizing to eight-digit precision, the coefficient should be
1.11111111e75(not1.11111111e76). Please update the test accordingly:- assertEq(signedCoefficient, 1.11111111e76); + assertEq(signedCoefficient, 1.11111111e75);If you keep the same normalization logic, also adjust the exponent assertion to:
- assertEq(exponent, -76 + 9); + assertEq(exponent, -75 + 9);
♻️ Duplicate comments (3)
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol (2)
26-27: Harmonize numeric literal style for mantissa (use integer form)Use the integer literal for the negative mantissa, mirroring the positive case and avoiding rational-literal readability/rounding pitfalls.
- checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -0.4304950862250382e16); + checkCharacteristicMantissa(-5.4304950862250382e16, -16, -5e16, -4304950862250382);
22-48: Add a fuzz invariant test over exponent ∈ [-77, 0]Property-based coverage will catch regressions without having to handcraft many examples. This mirrors previous suggestions and stays pure-compatible with forge-std.
Add this function to the contract:
function testFuzzCharacteristicMantissaInvariants(int256 signedCoefficient, int256 exponent) public pure { // Limit to exponents where the scale is well-defined and safe to compute exponent = bound(exponent, -77, 0); (int256 c, int256 m) = LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent); if (exponent == -77) { // scale = 1e77 > int256.max, so characteristic must be 0 and mantissa equals coefficient assertEq(c, 0, "Characteristic at -77 must be 0"); assertEq(m, signedCoefficient, "Mantissa at -77 must equal coefficient"); } else { // For -76..0 we can safely compute scale as int256. uint256 scaleU = 10 ** uint256(uint256(-exponent)); // <= 1e76 fits int256 int256 scale = int256(scaleU); // Decomposition invariant assertEq(c + m, signedCoefficient, "Decomposition invariant"); // Characteristic is a multiple of scale assertEq(c % scale, 0, "Characteristic must be multiple of scale"); // Mantissa magnitude bound: -scale < m < scale assertTrue(m > -scale && m < scale, "Mantissa magnitude bound"); // Mantissa sign follows coefficient when non-zero if (m != 0) { assertEq(m > 0, signedCoefficient > 0, "Mantissa sign follows coefficient"); } } }test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (1)
37-41: Test function name has been corrected.The test function name has been properly corrected from
testOverflowChekstotestOverflowChecks, addressing the previous typo issue.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (2)
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol(5 hunks)test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-11T14:30:48.562Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#96
File: test/src/lib/LibDecimalFloat.ceil.t.sol:43-50
Timestamp: 2025-08-11T14:30:48.562Z
Learning: When reviewing Solidity test files using Forge, verify the actual mutability of helper functions like `bound()` before suggesting changes to function mutability specifiers, as Forge implements many test utilities as pure functions.
Applied to files:
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.soltest/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol
📚 Learning: 2025-06-16T13:17:28.513Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: src/concrete/DecimalFloat.sol:175-182
Timestamp: 2025-06-16T13:17:28.513Z
Learning: In the rainlanguage/rain.math.float codebase, there's an established naming convention where functions accepting a `Float` type parameter consistently use `float` as the parameter name, even though it shadows the type name. This pattern is used throughout `LibDecimalFloat.sol` and should be maintained for consistency in related contracts like `DecimalFloat.sol`.
Applied to files:
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: rainix (ubuntu-latest, test-wasm-build)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
- GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
- GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
- GitHub Check: rainix (macos-latest, rainix-rs-test)
- GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
- GitHub Check: git-clean
🔇 Additional comments (9)
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol (1)
1-7: Good scaffolding and importsLicense, pinned pragma, and imports are consistent with the codebase. No issues.
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (8)
13-35: Well-designed overflow detection utilities.The two overflow detection functions provide good coverage for testing overflow scenarios.
willOverflowuses mathematical bounds checking whilewillOverflow2uses assembly-based sign comparison, providing comprehensive validation paths.
99-102: LGTM - Consistent with maximize behavior.The test correctly verifies that adding two pre-maximized values maintains their maximized form, which aligns with the new normalization strategy.
243-243: Good addition of fuzz-discovered edge case.Adding the specific edge case that was flagged during fuzzing (
54304950862250382, -16) is excellent for regression testing and ensuring the implementation handles corner cases correctly.
248-248: Test function renamed appropriately.The function name has been updated from
testAddSameExponentSameCoefficienttotestAddSameExponent, which better reflects its actual functionality of testing addition with matching exponents regardless of coefficient values.
261-266: Proper overflow handling in test logic.The addition of overflow detection logic ensures the test focuses on non-overflow cases, which is appropriate for this specific test. The use of
willOverflowfunction andvm.assumeproperly filters out problematic inputs.
94-96: Normalization via 1e76 scale is working as intendedThe
LibDecimalFloatImplementation.addtests now use pre-maximized inputs—e.g.(1e76, -76)to represent 1—and assert the result(2e76, -76)for 2. That aligns exactly with the maximize logic and the wrapper’s normalization inLibDecimalFloat. No further changes needed here.
116-118: Mixed exponent addition calculation is correctThe test in
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol(lines 116–118) correctly asserts:
- signedCoefficient = 1.23456789987654321e76
- exponent = -76 + 17 (= -59)
These values yield
1.23456789987654321e76 × 10^(–59) = 1.23456789987654321 × 10^17,
which matches 123 456 789e9 + 987 654 321. No changes needed.
172-174: Baseline test expectations are correctThe updated expected coefficients and exponents in
test/src/lib/implementation/LibDecimalFloatImplementation.add.t.sol (lines 172–174)
accurately reflect the exact sum of 1e37 and 1e0 when using maximized precision. No changes are needed.
| function checkCharacteristicMantissa( | ||
| int256 signedCoefficient, | ||
| int256 exponent, | ||
| int256 expectedCharacteristic, | ||
| int256 expectedMantissa | ||
| ) internal pure { | ||
| (int256 actualCharacteristic, int256 actualMantissa) = | ||
| LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent); | ||
|
|
||
| assertEq(actualCharacteristic, expectedCharacteristic, "Characteristic mismatch"); | ||
| assertEq(actualMantissa, expectedMantissa, "Mantissa mismatch"); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Augment helper with invariants to harden correctness
On top of golden-value checks, assert structural invariants (decomposition, bounds, divisibility by scale) for exponent ranges where they’re well-defined. This catches regressions without duplicating test cases.
Apply this diff to extend the helper:
) internal pure {
(int256 actualCharacteristic, int256 actualMantissa) =
LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent);
assertEq(actualCharacteristic, expectedCharacteristic, "Characteristic mismatch");
assertEq(actualMantissa, expectedMantissa, "Mantissa mismatch");
+ // Invariants:
+ // - Only meaningful for exponent <= 0 (scale >= 1).
+ // - For exponent == -77, scale = 1e77 > int256.max so characteristic must be 0 and mantissa == coefficient.
+ if (exponent <= 0) {
+ if (exponent == -77) {
+ assertEq(actualCharacteristic, 0, "Characteristic at -77 must be 0");
+ assertEq(actualMantissa, signedCoefficient, "Mantissa at -77 must equal coefficient");
+ } else {
+ // For -76..0, 10**(-exponent) fits in int256.
+ uint256 scaleU = 10 ** uint256(uint256(-exponent)); // <= 1e76
+ int256 scale = int256(scaleU);
+
+ // Decomposition invariant
+ assertEq(actualCharacteristic + actualMantissa, signedCoefficient, "Decomposition invariant");
+ // Characteristic is a multiple of scale
+ assertEq(actualCharacteristic % scale, 0, "Characteristic must be multiple of scale");
+ // Mantissa magnitude bound: -scale < m < scale
+ assertTrue(actualMantissa > -scale && actualMantissa < scale, "Mantissa magnitude bound");
+ // Mantissa sign follows coefficient when non-zero
+ if (actualMantissa != 0) {
+ assertEq(actualMantissa > 0, signedCoefficient > 0, "Mantissa sign follows coefficient");
+ }
+ }
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function checkCharacteristicMantissa( | |
| int256 signedCoefficient, | |
| int256 exponent, | |
| int256 expectedCharacteristic, | |
| int256 expectedMantissa | |
| ) internal pure { | |
| (int256 actualCharacteristic, int256 actualMantissa) = | |
| LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent); | |
| assertEq(actualCharacteristic, expectedCharacteristic, "Characteristic mismatch"); | |
| assertEq(actualMantissa, expectedMantissa, "Mantissa mismatch"); | |
| } | |
| function checkCharacteristicMantissa( | |
| int256 signedCoefficient, | |
| int256 exponent, | |
| int256 expectedCharacteristic, | |
| int256 expectedMantissa | |
| ) internal pure { | |
| (int256 actualCharacteristic, int256 actualMantissa) = | |
| LibDecimalFloatImplementation.characteristicMantissa(signedCoefficient, exponent); | |
| assertEq(actualCharacteristic, expectedCharacteristic, "Characteristic mismatch"); | |
| assertEq(actualMantissa, expectedMantissa, "Mantissa mismatch"); | |
| // Invariants: | |
| // - Only meaningful for exponent <= 0 (scale >= 1). | |
| // - For exponent == -77, scale = 1e77 > int256.max so characteristic must be 0 and mantissa == coefficient. | |
| if (exponent <= 0) { | |
| if (exponent == -77) { | |
| assertEq(actualCharacteristic, 0, "Characteristic at -77 must be 0"); | |
| assertEq(actualMantissa, signedCoefficient, "Mantissa at -77 must equal coefficient"); | |
| } else { | |
| // For -76..0, 10**(-exponent) fits in int256. | |
| uint256 scaleU = 10 ** uint256(uint256(-exponent)); // <= 1e76 | |
| int256 scale = int256(scaleU); | |
| // Decomposition invariant | |
| assertEq(actualCharacteristic + actualMantissa, signedCoefficient, "Decomposition invariant"); | |
| // Characteristic is a multiple of scale | |
| assertEq(actualCharacteristic % scale, 0, "Characteristic must be multiple of scale"); | |
| // Mantissa magnitude bound: -scale < m < scale | |
| assertTrue(actualMantissa > -scale && actualMantissa < scale, "Mantissa magnitude bound"); | |
| // Mantissa sign follows coefficient when non-zero | |
| if (actualMantissa != 0) { | |
| assertEq(actualMantissa > 0, signedCoefficient > 0, "Mantissa sign follows coefficient"); | |
| } | |
| } | |
| } | |
| } |
| // Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0 | ||
| checkCharacteristicMantissa(1, -77, 0, 1); | ||
| checkCharacteristicMantissa(-1, -77, 0, -1); |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Cover exponent < -77 as well (-78) to nail down “always zero characteristic” region
Given the documented semantics, exponents below -77 should behave the same as -77. Add a quick pair to make that explicit.
// Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0
checkCharacteristicMantissa(1, -77, 0, 1);
checkCharacteristicMantissa(-1, -77, 0, -1);
+ // And below -77, the same semantics hold
+ checkCharacteristicMantissa(1, -78, 0, 1);
+ checkCharacteristicMantissa(-1, -78, 0, -1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0 | |
| checkCharacteristicMantissa(1, -77, 0, 1); | |
| checkCharacteristicMantissa(-1, -77, 0, -1); | |
| // Beyond boundary at exponent -77 (scale > int256.max): characteristic must be 0 | |
| checkCharacteristicMantissa(1, -77, 0, 1); | |
| checkCharacteristicMantissa(-1, -77, 0, -1); | |
| // And below -77, the same semantics hold | |
| checkCharacteristicMantissa(1, -78, 0, 1); | |
| checkCharacteristicMantissa(-1, -78, 0, -1); |
🤖 Prompt for AI Agents
In
test/src/lib/implementation/LibDecimalFloatImplementation.characteristicMantissa.t.sol
around lines 45 to 47, the test covers exponent -77 but not -78; add the same
two assertions for exponent -78 to ensure values below -77 also yield a zero
characteristic — specifically add checkCharacteristicMantissa(1, -78, 0, 1) and
checkCharacteristicMantissa(-1, -78, 0, -1) immediately after the existing -77
checks.
Motivation
when we "maximize" a float we are trying to move the signed coefficient as high as we possibly can (high as in high bits) so that when we do subsequent operations such as adding, dividing, etc. we have as much room as possible in the low bits to preserve precision in fractional results
as our floats are decimal floats, they have an exponent that is base 10, but the underlying binary is base 2, because it is binary
this means the max possible signed integer value is 2^255-1 (255 not 256 because the high bit is for the sign), which is not an integer multiple of 10
currently we handle this in maximize by simply truncating anything between 1e75 and the max value (5.78e76) by dividing by 10 and raising the exponent by 1
this has two drawbacks:
in this PR, instead we check whether we can push the precision higher without overflowing due to the maximization process
the main advantage to this is that we no longer lose precision maximizing, we can only keep or increase it.
the exponent might now underflow due to subtracting 1, but this seems less likely to cause issues than the inverse
flow on effect is that addition needs new logic to catch overflows.
this is because addition previously assumed that maximized values can never overflow due to addition, but this assumption is no longer valid
Notably this will help resolve other precision issues like #31
Solution
Checks
By submitting this for review, I'm confirming I've done the following:
Summary by CodeRabbit
Bug Fixes
Tests