Skip to content

pack 0 standardized#136

Merged
thedavidmeister merged 4 commits intomainfrom
2025-09-16-exponent-overflow-zero
Sep 16, 2025
Merged

pack 0 standardized#136
thedavidmeister merged 4 commits intomainfrom
2025-09-16-exponent-overflow-zero

Conversation

@thedavidmeister
Copy link
Copy Markdown
Contributor

@thedavidmeister thedavidmeister commented Sep 16, 2025

Motivation

when packing 0 coefficient always returns standard 0 float instead of sometimes erroring due to exponent

when packing if exponent does not fit in int32 and is negative, return lossy 0 instead of erroring

Solution

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • Bug Fixes

    • Canonical zero representation when packing decimals — zero results now carry exponent 0.
    • Safer exponent overflow/underflow handling to avoid incorrect or lossy values.
  • Performance

    • Slightly faster zero packing via an early-return path for true zeros.
  • Tests

    • Added tests for zero packing and exponent overflow cases.
    • Updated many tests to expect exponent 0 when results are zero (floor, frac, pack, abs, minus, decimal conversions).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 16, 2025

Walkthrough

Adds canonical-zero short-circuit and input-capture to LibDecimalFloat.packLossy, refines exponent overflow/underflow handling, and updates tests to expect exponent 0 when the resulting coefficient is zero; also adds tests for packing zero and exponent-overflow behavior.

Changes

Cohort / File(s) Summary
Packing logic
src/lib/LibDecimalFloat.sol
Implements canonical-zero short-circuit in packLossy (return (FLOAT_ZERO, true) when coefficient == 0), captures initial inputs for safer overflow handling, and refines exponent overflow/underflow return/revert semantics.
Pack tests
test/src/lib/LibDecimalFloat.pack.t.sol
Adds testPackZero, testPackExponentOverflow, and testPackNegativeExponentLossyZero; updates round-trip assertion to expect exponentOut == 0 when coefficient == 0.
Floor tests
test/src/lib/LibDecimalFloat.floor.t.sol
Adjusts expectations so floored-zero results use exponent 0 (introduces local y in some paths); updates boundary and example cases to expect exponent 0 when value is zero.
Abs tests
test/src/lib/LibDecimalFloat.abs.t.sol
Changes assertion to expect exponent 0 when result coefficient is zero, otherwise preserve exponent.
Decimal tests
test/src/lib/LibDecimalFloat.decimal.t.sol
Updates unpacked exponent expectation to 0 when the unpacked coefficient is zero, otherwise match input exponent.
DecimalLossless tests
test/src/lib/LibDecimalFloat.decimalLossless.t.sol
Makes exponentPacked assertion conditional: zero coefficient implies exponentPacked == 0, else equals input exponent.
Frac tests
test/src/lib/LibDecimalFloat.frac.t.sol
Adjusts fractional-part expectations: when fractional part is zero set exponent 0; computes explicit local y for fractional value in-range.
Minus tests
test/src/lib/LibDecimalFloat.minus.t.sol
Updates assertion to expect exponentMinus == 0 when resulting coefficient is zero, otherwise preserve exponent.
Rust tests
crates/float/src/lib.rs
Changes test to expect underflow case returns zero Float (is_zero) instead of raising ExponentOverflow error.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller as Caller
  participant Lib as LibDecimalFloat.packLossy

  Caller->>Lib: packLossy(signedCoefficient, exponent)
  alt signedCoefficient == 0
    rect rgb(230,245,255)
      Note over Lib: Capture inputs\nReturn canonical zero (lossless)
      Lib-->>Caller: (FLOAT_ZERO, true)
    end
  else
    rect rgb(245,245,245)
      Lib->>Lib: normalize coefficient & adjust exponent
      Lib->>Lib: check exponent bounds (int32)
      alt exponent overflow/underflow
        rect rgb(255,235,235)
          Note over Lib: Use captured initial inputs\nRevert or return lossy zero as appropriate
          Lib-->>Caller: revert(ExponentOverflow) or (FLOAT_ZERO,false)
        end
      else
        Lib-->>Caller: (packedFloat, losslessFlag)
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Expose lossy conversion #83 — Directly related to lossy packing/conversion semantics and tests.
  • pow 0 #87 — Related canonical-zero and pow/zero-case logic impacting packLossy behavior.
  • 2025 04 15 pack #28 — Overlaps on packing/unpacking normalization and exponent bounds handling.

Suggested reviewers

  • hardyjosh
  • findolor

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "pack 0 standardized" succinctly and accurately reflects the PR's primary goal—standardizing the packing behavior for a zero coefficient (and related exponent handling)—without extraneous detail, making the main intent clear to reviewers scanning history. It is concise, relevant to the changes in LibDecimalFloat.packLossy and the updated tests, and communicates the developer's perspective on the core change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2025-09-16-exponent-overflow-zero

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c67a63 and 06ba31e.

📒 Files selected for processing (1)
  • crates/float/src/lib.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#111
File: test/src/lib/LibDecimalFloat.div.t.sol:48-51
Timestamp: 2025-08-26T16:50:11.113Z
Learning: In LibDecimalFloat, packLossless internally asserts that the packing operation is lossless, so there's no need to manually check the lossless flag when using packLossless instead of packLossy.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: test/src/lib/LibDecimalFloat.decimal.t.sol:122-126
Timestamp: 2025-08-29T14:54:24.240Z
Learning: In test/src/lib/LibDecimalFloat.decimal.t.sol, the fromFixedDecimalLossy function converts uint256(type(int256).max) losslessly because the boundary check is `value > uint256(type(int256).max)`, not `>=`. Values exactly equal to type(int256).max still take the lossless conversion path.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: test/src/lib/LibDecimalFloat.decimal.t.sol:122-126
Timestamp: 2025-08-29T14:54:24.240Z
Learning: In test/src/lib/LibDecimalFloat.decimal.t.sol, the fromFixedDecimalLossy function converts uint256(type(int256).max) losslessly because the boundary check is `value > uint256(type(int256).max)`, not `>=`. Values exactly equal to type(int256).max still take the lossless conversion path.
📚 Learning: 2025-09-09T15:11:31.003Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.

Applied to files:

  • crates/float/src/lib.rs
📚 Learning: 2025-06-18T09:10:41.740Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#57
File: crates/float/src/lib.rs:316-328
Timestamp: 2025-06-18T09:10:41.740Z
Learning: In the rain.math.float codebase, the user prefers implementing standard Rust traits (like Neg) rather than creating redundant public methods when the trait already provides the needed functionality. Float implements Copy, so reference usage with operators is not a concern.

Applied to files:

  • crates/float/src/lib.rs
📚 Learning: 2025-06-11T09:03:20.771Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#46
File: crates/float/proptest-regressions/lib.txt:1-3
Timestamp: 2025-06-11T09:03:20.771Z
Learning: The file `crates/float/proptest-regressions/lib.txt` is automatically generated by proptest and should not be manually edited.

Applied to files:

  • crates/float/src/lib.rs
⏰ 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). (9)
  • GitHub Check: rainix (ubuntu-latest, test-wasm-build)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-legal)
  • GitHub Check: rainix (macos-latest, rainix-sol-legal)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
  • GitHub Check: rainix (macos-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
  • GitHub Check: git-clean
🔇 Additional comments (1)
crates/float/src/lib.rs (1)

1546-1548: Underflow now returns canonical zero — strengthen test and rename it.

In crates/float/src/lib.rs (test near lines 1546–1548):

         let float = (near_min_exp * one_e_neg_three).unwrap();
-        assert!(float.is_zero().unwrap());
+        assert!(float.is_zero().unwrap());
+        // Canonical zero should be coefficient 0, exponent 0.
+        assert_eq!(float.show_unpacked().unwrap(), "0e0");
+        // Human-format should be "0".
+        assert_eq!(float.format().unwrap(), "0");
- fn test_mul_exponent_underflow_error() {
+ fn test_mul_exponent_underflow_to_zero() {

Verification note: the provided search failed with "crates/float/test: No such file or directory". Re-run from the repo root to confirm no other tests expect an underflow error:

rg -n -C2 -iP 'underflow.*error|error.*underflow' --type rust crates/float


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85fd3f2 and 7aaa419.

📒 Files selected for processing (7)
  • src/lib/LibDecimalFloat.sol (2 hunks)
  • test/src/lib/LibDecimalFloat.abs.t.sol (1 hunks)
  • test/src/lib/LibDecimalFloat.decimal.t.sol (1 hunks)
  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol (1 hunks)
  • test/src/lib/LibDecimalFloat.frac.t.sol (1 hunks)
  • test/src/lib/LibDecimalFloat.minus.t.sol (1 hunks)
  • test/src/lib/LibDecimalFloat.pack.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#111
File: test/src/lib/LibDecimalFloat.div.t.sol:48-51
Timestamp: 2025-08-26T16:50:11.113Z
Learning: In LibDecimalFloat, packLossless internally asserts that the packing operation is lossless, so there's no need to manually check the lossless flag when using packLossless instead of packLossy.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.
📚 Learning: 2025-08-29T14:54:24.240Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: test/src/lib/LibDecimalFloat.decimal.t.sol:122-126
Timestamp: 2025-08-29T14:54:24.240Z
Learning: In test/src/lib/LibDecimalFloat.decimal.t.sol, the fromFixedDecimalLossy function converts uint256(type(int256).max) losslessly because the boundary check is `value > uint256(type(int256).max)`, not `>=`. Values exactly equal to type(int256).max still take the lossless conversion path.

Applied to files:

  • test/src/lib/LibDecimalFloat.minus.t.sol
  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
  • src/lib/LibDecimalFloat.sol
  • test/src/lib/LibDecimalFloat.abs.t.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-09-09T15:11:31.003Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.

Applied to files:

  • test/src/lib/LibDecimalFloat.minus.t.sol
  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.frac.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
  • src/lib/LibDecimalFloat.sol
  • test/src/lib/LibDecimalFloat.abs.t.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-21T18:03:40.347Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#107
File: test/lib/LibDecimalFloatSlow.sol:37-45
Timestamp: 2025-08-21T18:03:40.347Z
Learning: In test/lib/LibDecimalFloatSlow.sol, the "slow" implementation is intentionally different from the production implementation to serve as an independent reference for fuzzing tests. The goal is to have two different approaches (expensive loops vs optimized jumps) that produce equivalent results, not identical implementations.

Applied to files:

  • test/src/lib/LibDecimalFloat.minus.t.sol
  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.frac.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
  • test/src/lib/LibDecimalFloat.abs.t.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-26T16:50:11.113Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#111
File: test/src/lib/LibDecimalFloat.div.t.sol:48-51
Timestamp: 2025-08-26T16:50:11.113Z
Learning: In LibDecimalFloat, packLossless internally asserts that the packing operation is lossless, so there's no need to manually check the lossless flag when using packLossless instead of packLossy.

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
  • src/lib/LibDecimalFloat.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-09-10T19:20:35.927Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#135
File: test/src/lib/LibDecimalFloat.pow.t.sol:160-161
Timestamp: 2025-09-10T19:20:35.927Z
Learning: In test/src/lib/LibDecimalFloat.pow.t.sol, the comment "we do not try" in the context of "The round trip should not error so we do not try" refers to not using try/catch constructs around the powExternal call, contrasting with previous versions that used nested try/catch blocks.

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
📚 Learning: 2025-06-17T10:02:01.394Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#64
File: src/concrete/DecimalFloat.sol:0-0
Timestamp: 2025-06-17T10:02:01.394Z
Learning: In the rain.math.float project, the Float type is designed with a specific bit layout: 224-bit signed coefficient and 32-bit signed exponent, represented as 32 bytes total. The LibDecimalFloat.unpack function returns values that are guaranteed to fit within int224 and int32 ranges due to this internal representation, making explicit range checks before casting unnecessary.

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
📚 Learning: 2025-09-02T09:33:32.513Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#119
File: test/src/lib/format/LibFormatDecimalFloat.toDecimalString.t.sol:174-175
Timestamp: 2025-09-02T09:33:32.513Z
Learning: The LibFormatDecimalFloat.toDecimalString function in src/lib/format/LibFormatDecimalFloat.sol does not include rounding logic. It formats decimal floats as-is without rounding values based on significant figures limits.

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
📚 Learning: 2025-08-29T10:38:26.353Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:593-601
Timestamp: 2025-08-29T10:38:26.353Z
Learning: The maximize function in LibDecimalFloatImplementation.sol produces exact results for simple integer values like 1. maximize(1, 0) yields exactly (1e76, -76) with no precision loss, and the log10 special case for signedCoefficient == 1e76 correctly handles this.

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • src/lib/LibDecimalFloat.sol
  • test/src/lib/LibDecimalFloat.abs.t.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 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.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.decimal.t.sol
  • src/lib/LibDecimalFloat.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-29T10:38:26.353Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:593-601
Timestamp: 2025-08-29T10:38:26.353Z
Learning: In Solidity, int256(1) when passed through the maximize function in LibDecimalFloatImplementation.sol produces exactly (1e76, -76), not an approximation. This means the special case for signedCoefficient == 1e76 in log10 correctly handles powers of 10 like log10(1).

Applied to files:

  • test/src/lib/LibDecimalFloat.decimalLossless.t.sol
  • test/src/lib/LibDecimalFloat.abs.t.sol
  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-29T14:58:50.500Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:896-899
Timestamp: 2025-08-29T14:58:50.500Z
Learning: In unchecked Solidity blocks, arithmetic operations can overflow/underflow and wrap around, so bounds checks that seem "impossible" for normal arithmetic may actually be necessary to catch overflow edge cases. For example, in withTargetExponent function, the check `exponentDiff < 0` is needed because `targetExponent - exponent` could underflow in unchecked arithmetic.

Applied to files:

  • test/src/lib/LibDecimalFloat.frac.t.sol
  • test/src/lib/LibDecimalFloat.pack.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). (8)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-legal)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
  • GitHub Check: rainix (macos-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
  • GitHub Check: rainix (ubuntu-latest, test-wasm-build)
  • GitHub Check: git-clean
🔇 Additional comments (9)
src/lib/LibDecimalFloat.sol (2)

317-321: Canonical-zero handling LGTM.

Zero coefficient packing to FLOAT_ZERO irrespective of input exponent is consistent and simplifies downstream behavior.


323-330: Clarify underflow vs overflow classification using original exponent.

Negative out‑of‑range exponents should only underflow-to-zero when the original input exponent was already negative; otherwise this path could misclassify. Use initialExponent to decide.

Apply this diff:

-            if (int32(exponent) != exponent) {
-                // If the exponent is negative then this is a number too small
-                // to pack. We return zero but it is not a lossless conversion.
-                if (exponent < 0) {
-                    return (FLOAT_ZERO, false);
-                }
-                revert ExponentOverflow(initialSignedCoefficient, initialExponent);
-            }
+            if (int32(exponent) != exponent) {
+                // Underflow to zero only if the original exponent was negative.
+                if (initialExponent < 0) {
+                    return (FLOAT_ZERO, false);
+                }
+                revert ExponentOverflow(initialSignedCoefficient, initialExponent);
+            }
⛔ Skipped due to learnings
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:896-899
Timestamp: 2025-08-29T14:58:50.500Z
Learning: In unchecked Solidity blocks, arithmetic operations can overflow/underflow and wrap around, so bounds checks that seem "impossible" for normal arithmetic may actually be necessary to catch overflow edge cases. For example, in withTargetExponent function, the check `exponentDiff < 0` is needed because `targetExponent - exponent` could underflow in unchecked arithmetic.
test/src/lib/LibDecimalFloat.decimal.t.sol (1)

34-34: Expectation updated to canonical-zero exponent is correct.

exponentFloat should be 0 whenever the packed coefficient is 0.

test/src/lib/LibDecimalFloat.abs.t.sol (1)

19-19: Abs test aligns with canonical-zero.

Clamping result exponent to 0 when coefficient is 0 is correct.

test/src/lib/LibDecimalFloat.minus.t.sol (1)

29-29: Minus test matches new packing semantics.

Zero coefficient implies exponent 0 on unpack; assertion is correct.

test/src/lib/LibDecimalFloat.decimalLossless.t.sol (1)

48-48: Lossless packed round-trip assertion updated appropriately.

Conditionally expecting exponent 0 for zero coefficient is correct.

test/src/lib/LibDecimalFloat.frac.t.sol (1)

26-26: Frac tests correctly adopt canonical-zero behavior and explicit modulo.

  • Nonnegative exponent → zero fractional part with exponent 0.
  • For very small numbers, exponent 0 when fractional part is 0.
  • Using y = x % 10**(-exponent) tightens expectations and readability.

Also applies to: 33-33, 39-41

test/src/lib/LibDecimalFloat.pack.t.sol (2)

25-26: Round-trip exponent assertion updated correctly.

Ensures packed zero normalizes exponent to 0.


28-33: Good: explicit zero-pack test.

Validates both losslessness and canonical FLOAT_ZERO.

Comment thread src/lib/LibDecimalFloat.sol
Comment thread test/src/lib/LibDecimalFloat.pack.t.sol
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
test/src/lib/LibDecimalFloat.pack.t.sol (1)

44-49: Resolved: underflow-to-zero path now covered.

This addresses the earlier request to add coverage for negative exponent underflow that yields lossy zero.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7aaa419 and 9c67a63.

📒 Files selected for processing (1)
  • test/src/lib/LibDecimalFloat.pack.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#111
File: test/src/lib/LibDecimalFloat.div.t.sol:48-51
Timestamp: 2025-08-26T16:50:11.113Z
Learning: In LibDecimalFloat, packLossless internally asserts that the packing operation is lossless, so there's no need to manually check the lossless flag when using packLossless instead of packLossy.
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.
📚 Learning: 2025-08-29T14:54:24.240Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: test/src/lib/LibDecimalFloat.decimal.t.sol:122-126
Timestamp: 2025-08-29T14:54:24.240Z
Learning: In test/src/lib/LibDecimalFloat.decimal.t.sol, the fromFixedDecimalLossy function converts uint256(type(int256).max) losslessly because the boundary check is `value > uint256(type(int256).max)`, not `>=`. Values exactly equal to type(int256).max still take the lossless conversion path.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-09-09T15:11:31.003Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#130
File: src/lib/implementation/LibDecimalFloatImplementation.sol:320-330
Timestamp: 2025-09-09T15:11:31.003Z
Learning: In LibDecimalFloatImplementation.sol div function, returning zero when underflowExponentBy > 76 is intentional "non-error underflow" behavior rather than reverting. This graceful handling is the main goal of the changes.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-26T16:50:11.113Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#111
File: test/src/lib/LibDecimalFloat.div.t.sol:48-51
Timestamp: 2025-08-26T16:50:11.113Z
Learning: In LibDecimalFloat, packLossless internally asserts that the packing operation is lossless, so there's no need to manually check the lossless flag when using packLossless instead of packLossy.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-21T18:03:40.347Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#107
File: test/lib/LibDecimalFloatSlow.sol:37-45
Timestamp: 2025-08-21T18:03:40.347Z
Learning: In test/lib/LibDecimalFloatSlow.sol, the "slow" implementation is intentionally different from the production implementation to serve as an independent reference for fuzzing tests. The goal is to have two different approaches (expensive loops vs optimized jumps) that produce equivalent results, not identical implementations.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 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.pack.t.sol
📚 Learning: 2025-08-29T14:58:50.500Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:896-899
Timestamp: 2025-08-29T14:58:50.500Z
Learning: In unchecked Solidity blocks, arithmetic operations can overflow/underflow and wrap around, so bounds checks that seem "impossible" for normal arithmetic may actually be necessary to catch overflow edge cases. For example, in withTargetExponent function, the check `exponentDiff < 0` is needed because `targetExponent - exponent` could underflow in unchecked arithmetic.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-29T10:38:26.353Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:593-601
Timestamp: 2025-08-29T10:38:26.353Z
Learning: In Solidity, int256(1) when passed through the maximize function in LibDecimalFloatImplementation.sol produces exactly (1e76, -76), not an approximation. This means the special case for signedCoefficient == 1e76 in log10 correctly handles powers of 10 like log10(1).

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-08-29T10:38:26.353Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#115
File: src/lib/implementation/LibDecimalFloatImplementation.sol:593-601
Timestamp: 2025-08-29T10:38:26.353Z
Learning: The maximize function in LibDecimalFloatImplementation.sol produces exact results for simple integer values like 1. maximize(1, 0) yields exactly (1e76, -76) with no precision loss, and the log10 special case for signedCoefficient == 1e76 correctly handles this.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.t.sol
📚 Learning: 2025-06-17T10:02:01.394Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#64
File: src/concrete/DecimalFloat.sol:0-0
Timestamp: 2025-06-17T10:02:01.394Z
Learning: In the rain.math.float project, the Float type is designed with a specific bit layout: 224-bit signed coefficient and 32-bit signed exponent, represented as 32 bytes total. The LibDecimalFloat.unpack function returns values that are guaranteed to fit within int224 and int32 ranges due to this internal representation, making explicit range checks before casting unnecessary.

Applied to files:

  • test/src/lib/LibDecimalFloat.pack.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). (8)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
  • GitHub Check: rainix (ubuntu-latest, test-wasm-build)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-legal)
  • GitHub Check: rainix (macos-latest, rainix-rs-test)
  • GitHub Check: git-clean
🔇 Additional comments (1)
test/src/lib/LibDecimalFloat.pack.t.sol (1)

25-26: LGTM: canonical-zero round‑trip assertion is correct.

Asserting exponentOut == 0 when signedCoefficient == 0 aligns with the canonical-zero rule and removes ambiguity in expectations.

Comment thread test/src/lib/LibDecimalFloat.pack.t.sol
Comment thread test/src/lib/LibDecimalFloat.pack.t.sol
Comment on lines +44 to +49
function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external view {
exponent = bound(exponent, type(int256).min, int256(type(int32).min) - 77);
vm.assume(signedCoefficient != 0);
(Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
assertFalse(lossless, "lossless");
assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Tighten the underflow test: make it pure and target the threshold precisely.

  • Mark the test pure (no state read/write).
  • Bound signedCoefficient to ≥1 to avoid discards.
  • Pin exponent to a value ≤ int32.min − 77 to deterministically exercise the lossy‑zero path.

Apply this diff:

-    function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external view {
-        exponent = bound(exponent, type(int256).min, int256(type(int32).min) - 77);
-        vm.assume(signedCoefficient != 0);
+    function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external pure {
+        signedCoefficient = bound(signedCoefficient, 1, type(int256).max);
+        exponent = bound(exponent, int256(type(int32).min) - 77, int256(type(int32).min) - 77);
         (Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
         assertFalse(lossless, "lossless");
         assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
     }

Consider adding a sibling test for the “within 1..76” underflow window to assert non‑zero, lossy pack clamped at int32.min.

📝 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.

Suggested change
function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external view {
exponent = bound(exponent, type(int256).min, int256(type(int32).min) - 77);
vm.assume(signedCoefficient != 0);
(Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
assertFalse(lossless, "lossless");
assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external pure {
signedCoefficient = bound(signedCoefficient, 1, type(int256).max);
exponent = bound(exponent, int256(type(int32).min) - 77, int256(type(int32).min) - 77);
(Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
assertFalse(lossless, "lossless");
assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
}

@thedavidmeister thedavidmeister merged commit 6358302 into main Sep 16, 2025
10 checks passed
@github-actions
Copy link
Copy Markdown

@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment:

S/M/L PR Classification Guidelines:

This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed.

Small (S)

Characteristics:

  • Simple bug fixes, typos, or minor refactoring
  • Single-purpose changes affecting 1-2 files
  • Documentation updates
  • Configuration tweaks
  • Changes that require minimal context to review

Review Effort: Would have taken 5-10 minutes

Examples:

  • Fix typo in variable name
  • Update README with new instructions
  • Adjust configuration values
  • Simple one-line bug fixes
  • Import statement cleanup

Medium (M)

Characteristics:

  • Feature additions or enhancements
  • Refactoring that touches multiple files but maintains existing behavior
  • Breaking changes with backward compatibility
  • Changes requiring some domain knowledge to review

Review Effort: Would have taken 15-30 minutes

Examples:

  • Add new feature or component
  • Refactor common utility functions
  • Update dependencies with minor breaking changes
  • Add new component with tests
  • Performance optimizations
  • More complex bug fixes

Large (L)

Characteristics:

  • Major feature implementations
  • Breaking changes or API redesigns
  • Complex refactoring across multiple modules
  • New architectural patterns or significant design changes
  • Changes requiring deep context and multiple review rounds

Review Effort: Would have taken 45+ minutes

Examples:

  • Complete new feature with frontend/backend changes
  • Protocol upgrades or breaking changes
  • Major architectural refactoring
  • Framework or technology upgrades

Additional Factors to Consider

When deciding between sizes, also consider:

  • Test coverage impact: More comprehensive test changes lean toward larger classification
  • Risk level: Changes to critical systems bump up a size category
  • Team familiarity: Novel patterns or technologies increase complexity

Notes:

  • the assessment must be for the totality of the PR, that means comparing the base branch to the last commit of the PR
  • the assessment output must be exactly one of: S, M or L (single-line comment) in format of: SIZE={S/M/L}
  • do not include any additional text, only the size classification
  • your assessment comment must not include tips or additional sections
  • do NOT tag me or anyone else on your comment

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 16, 2025

Tip

For best results, initiate chat on the files or code changes.

@coderabbitai coderabbitai Bot mentioned this pull request Jan 31, 2026
4 tasks
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