Skip to content

multivariadic sub#408

Merged
thedavidmeister merged 1 commit into
mainfrom
2025-09-19-multivariadic-sub
Sep 19, 2025
Merged

multivariadic sub#408
thedavidmeister merged 1 commit into
mainfrom
2025-09-19-multivariadic-sub

Conversation

@thedavidmeister
Copy link
Copy Markdown
Contributor

@thedavidmeister thedavidmeister commented Sep 19, 2025

Motivation

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

  • New Features
    • More consistent decimal subtraction results when processing multiple values.
  • Performance
    • Streamlined arithmetic path may reduce overhead in subtraction-heavy workloads.
  • Bug Fixes
    • Reduces edge-case discrepancies in decimal subtraction outcomes.
  • Refactor
    • Internal arithmetic pipeline revamped to accumulate results in an intermediate form and convert at boundaries.
    • No changes to public APIs; existing functionality preserved with improved consistency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 19, 2025

Walkthrough

Refactors LibOpSub to perform decimal float subtraction by unpacking operands into (signedCoefficient, exponent), using LibDecimalFloatImplementation.sub for arithmetic, and repacking the result with LibDecimalFloat.packLossy. Applies this pattern in both run() and referenceFn(), adds using LibDecimalFloat for Float, and handles lossless flags and slither suppressions.

Changes

Cohort / File(s) Summary
Decimal float subtraction refactor
src/lib/op/math/LibOpSub.sol
- Import LibDecimalFloatImplementation; add using LibDecimalFloat for Float
- Replace direct LibDecimalFloat.sub with unpack → LibDecimalFloatImplementation.subpackLossy pattern
- Apply to initial op and loop in run() and referenceFn()
- Capture/ignore lossless flag; add slither suppression for unused return

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Caller
    participant OpSub as LibOpSub.run/referenceFn
    participant Float as LibDecimalFloat (Float)
    participant Impl as LibDecimalFloatImplementation

    Caller->>OpSub: invoke with inputs (Float a, Float b, [...])
    OpSub->>Float: a.unpack()
    Float-->>OpSub: (aSignedCoeff, aExp)
    OpSub->>Float: b.unpack()
    Float-->>OpSub: (bSignedCoeff, bExp)
    OpSub->>Impl: sub(aSignedCoeff,aExp,bSignedCoeff,bExp)
    Impl-->>OpSub: (accSignedCoeff, accExp)

    loop for each additional operand
        OpSub->>Float: xi.unpack()
        Float-->>OpSub: (xiSignedCoeff, xiExp)
        OpSub->>Impl: sub(accSignedCoeff,accExp,xiSignedCoeff,xiExp)
        Impl-->>OpSub: (accSignedCoeff, accExp)
    end

    OpSub->>Float: packLossy(accSignedCoeff, accExp)
    Float-->>OpSub: (resultFloat, lossless)
    OpSub-->>Caller: resultFloat
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • multivariadic mul #407 — Similar refactor pattern: switch to unpack → LibDecimalFloatImplementation.* → packLossy with loss flag handling (mul vs sub).
  • sub #352 — Earlier change to LibOpSub implementing Float/LibDecimalFloat-based subtraction; this PR advances it to the unpack/implementation/packLossy approach.

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 "multivariadic sub" is short, focused, and directly describes the primary change in the diff—adding multivariadic subtraction behavior (the unpack-and-sub flow and repacking in LibOpSub.sol) as shown in the summary, so it accurately represents the main change and is clear for a teammate scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2025-09-19-multivariadic-sub

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: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c26b059 and 8664ff5.

⛔ Files ignored due to path filters (1)
  • src/generated/Rainterpreter.pointers.sol is excluded by !**/generated/**
📒 Files selected for processing (1)
  • src/lib/op/math/LibOpSub.sol (4 hunks)
⏰ 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). (10)
  • GitHub Check: git-clean
  • GitHub Check: rainix (ubuntu-latest, test-wasm-build)
  • GitHub Check: rainix (macos-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-artifacts)
  • GitHub Check: rainix (macos-latest, rainix-rs-artifacts)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-artifacts)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
🔇 Additional comments (1)
src/lib/op/math/LibOpSub.sol (1)

33-36: Cannot verify Float.unpack ↔ LibDecimalFloatImplementation.sub compatibility — library sources missing

Imports reference rain.math.float/lib/LibDecimalFloat.sol and rain.math.float/lib/implementation/LibDecimalFloatImplementation.sol (e.g., src/lib/op/math/LibOpSub.sol), but the referenced library sources are not present in the working tree; cannot confirm that Float.unpack()'s (signedCoefficient:int256, exponent:int256) ordering/sign/scale semantics match LibDecimalFloatImplementation.sub(...). Add the missing library sources or explicitly confirm unpack() returns (signedCoefficient, exponent) with the same semantics expected by the implementation.

import {InterpreterState} from "../../state/LibInterpreterState.sol";
import {Float, LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol";
import {StackItem} from "rain.interpreter.interface/interface/unstable/IInterpreterV4.sol";
import {LibDecimalFloatImplementation} from "rain.math.float/lib/implementation/LibDecimalFloatImplementation.sol";
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Sep 19, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Refactor introduces unnecessary complexity for decimal float subtraction

The refactoring from direct LibDecimalFloat.sub(a, b) calls to unpacking/repacking with LibDecimalFloatImplementation.sub() adds significant complexity without clear benefits. This pattern:

  1. Increases gas consumption due to multiple unpack/repack operations
  2. Reduces code readability with verbose coefficient/exponent handling
  3. Potentially loses precision through the packLossy operation

Unless there's a specific requirement for coefficient/exponent manipulation (e.g., custom rounding, overflow handling), the simpler LibDecimalFloat.sub() approach should be preferred.

Consider reverting to the simpler approach:

-import {LibDecimalFloatImplementation} from "rain.math.float/lib/implementation/LibDecimalFloatImplementation.sol";

 library LibOpSub {
-    using LibDecimalFloat for Float;
-

     function run(InterpreterState memory, OperandV2 operand, Pointer stackTop) internal pure returns (Pointer) {
         Float a;
         Float b;
         assembly ("memory-safe") {
             a := mload(stackTop)
             b := mload(add(stackTop, 0x20))
             stackTop := add(stackTop, 0x40)
         }
-        (int256 signedCoefficient, int256 exponent) = a.unpack();
-        (int256 signedCoefficientB, int256 exponentB) = b.unpack();
-        (signedCoefficient, exponent) =
-            LibDecimalFloatImplementation.sub(signedCoefficient, exponent, signedCoefficientB, exponentB);
+        a = LibDecimalFloat.sub(a, b);

         {
             uint256 inputs = uint256(OperandV2.unwrap(operand) >> 0x10) & 0x0F;
             uint256 i = 2;
             while (i < inputs) {
                 assembly ("memory-safe") {
                     b := mload(stackTop)
                     stackTop := add(stackTop, 0x20)
                 }
-                (signedCoefficientB, exponentB) = b.unpack();
-                (signedCoefficient, exponent) =
-                    LibDecimalFloatImplementation.sub(signedCoefficient, exponent, signedCoefficientB, exponentB);
+                a = LibDecimalFloat.sub(a, b);
                 unchecked {
                     i++;
                 }
             }
         }
-        //slither-disable-next-line unused-return
-        (a,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);

Also applies to: 15-15, 33-36, 46-48, 54-55, 74-85

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

unpacking and lossy conversion happens inside LibDecimalFloat.sub but not LibDecimalFloatImplementation.sub

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for LibDecimalFloat.sub implementation to understand the internal unpacking/packing
ast-grep --pattern 'function sub($_, $_) {
  $$$
}'

# Also search for LibDecimalFloatImplementation.sub to compare
rg -n -A 10 -B 2 "function sub" --type=sol

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for LibDecimalFloat.sub implementation
rg -n -A 10 -B 2 "function sub" --type-add 'sol:*.sol' --type=sol

# If that doesn't work, search more broadly
rg -n -A 10 -B 2 "function sub" -g "*.sol"

Length of output: 43045


🏁 Script executed:

#!/bin/bash
# Search specifically for mathematical subtraction functions in the codebase
rg -n -A 15 -B 5 "function sub" -g "*.sol" | grep -A 15 -B 5 -E "(Float|decimal|math)"

# Search for LibDecimalFloat and LibDecimalFloatImplementation in external dependencies
find . -name "*.sol" -exec grep -l "LibDecimalFloat" {} \;

# Let's look for the actual implementation differences by searching for specific functions
rg -n -A 10 -B 5 "LibDecimalFloat\.sub\|LibDecimalFloatImplementation\.sub" -g "*.sol"

Length of output: 3666


🏁 Script executed:

#!/bin/bash
# Let's check other math operations to see if they use similar patterns
cat src/lib/op/math/LibOpAdd.sol | head -50

# Check if other operations use LibDecimalFloatImplementation
rg -n "LibDecimalFloatImplementation" -g "*.sol"

# Look at the imports and usage in LibOpSub to understand the change better
cat src/lib/op/math/LibOpSub.sol | grep -A 5 -B 5 "Implementation\|unpack\|packLossy"

Length of output: 8529


@thedavidmeister Thank you for the clarification! You're absolutely right, and I understand now why this refactoring was necessary.

Looking at the other math operations (LibOpAdd, LibOpMul, LibOpDiv), I can see they all follow the same pattern: they use LibDecimalFloatImplementation functions which operate directly on unpacked coefficients and exponents, avoiding the internal unpacking/packing that happens inside LibDecimalFloat.sub().

This approach is more efficient for multi-operand operations because:

  1. Avoids redundant packing/unpacking: LibDecimalFloat.sub(a, b) would internally unpack both floats, perform the operation, then pack the result back to a Float. In a loop, this creates unnecessary intermediate packing.

  2. Better precision control: By using LibDecimalFloatImplementation.sub() directly on unpacked values and only calling packLossy() once at the end, you maintain maximum precision throughout the computation chain.

  3. Consistency: All the math operations (add, sub, mul, div) now follow the same pattern for multivariadic operations.

Your implementation correctly keeps the computation in the unpacked coefficient/exponent form throughout the loop and only packs the final result once, which is indeed more efficient than the previous approach that would have caused multiple pack/unpack cycles.


✏️ Learnings added
Learnt from: thedavidmeister
PR: rainlanguage/rain.interpreter#408
File: src/lib/op/math/LibOpSub.sol:10-10
Timestamp: 2025-09-19T17:18:14.810Z
Learning: LibDecimalFloat.sub() internally performs unpacking and packing with potential precision loss, while LibDecimalFloatImplementation.sub() works directly on unpacked coefficients and exponents without intermediate packing. For multivariadic operations, using LibDecimalFloatImplementation functions is more efficient as it avoids redundant pack/unpack cycles and maintains better precision control.

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.

Comment on lines +54 to +55
//slither-disable-next-line unused-return
(a,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
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

Potential precision loss from packLossy without warning

The packLossy function returns a lossless flag indicating whether precision was lost during packing, but this information is discarded. This could lead to silent precision loss in calculations.

Consider either:

  1. Using a lossless packing method if available
  2. Documenting that precision loss is expected/acceptable
  3. Reverting if precision loss occurs (if exactness is required)
-        //slither-disable-next-line unused-return
-        (a,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
+        bool lossless;
+        (a, lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
+        // Consider: require(lossless, "Precision loss in subtraction");
📝 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
//slither-disable-next-line unused-return
(a,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
bool lossless;
(a, lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
// Consider: require(lossless, "Precision loss in subtraction");
🤖 Prompt for AI Agents
In src/lib/op/math/LibOpSub.sol around lines 54-55, the call to
LibDecimalFloat.packLossy ignores the returned lossless flag which can hide
precision loss; update the call to capture the second return value (bool
lossless) and then either (a) call a lossless packing alternative if one exists,
(b) require(lossless, "precision loss") to revert when exactness is required, or
(c) explicitly document/emit an event when lossless is false if silent loss is
acceptable—implement the chosen handling and add a short code comment explaining
the decision.

Comment on lines +83 to +85
bool lossless;
(acc, lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
(lossless);
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.

🛠️ Refactor suggestion

Remove unnecessary variable assignment and parentheses

The lossless variable is captured but immediately discarded with (lossless);. This adds no value and creates confusion.

-            bool lossless;
-            (acc, lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
-            (lossless);
+            (acc,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
📝 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
bool lossless;
(acc, lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
(lossless);
(acc,) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
🤖 Prompt for AI Agents
In src/lib/op/math/LibOpSub.sol around lines 83 to 85, the function assigns the
boolean variable `lossless` from LibDecimalFloat.packLossy and then immediately
discards it with `(lossless);`. Remove the unused `lossless` variable and any
leftover discard expression, calling LibDecimalFloat.packLossy only into the
needed `acc` variable (or otherwise handle the returned boolean where it's
meaningful), so there is no redundant assignment or stray parentheses.

@thedavidmeister thedavidmeister merged commit 821f42a into main Sep 19, 2025
11 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

@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

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