Skip to content

expose constants#116

Merged
0xgleb merged 7 commits intomainfrom
feat/expose-constants
Sep 8, 2025
Merged

expose constants#116
0xgleb merged 7 commits intomainfrom
feat/expose-constants

Conversation

@0xgleb
Copy link
Copy Markdown
Contributor

@0xgleb 0xgleb commented Aug 27, 2025

Motivation

See #93

Solution

Expose and test Solidity methods for offchain use

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

    • Exposed four boundary float constants (max/min positive and max/min negative) in both JavaScript and Rust APIs for edge-case handling and comparisons.
  • Documentation

    • Added usage examples and notes describing return behavior and how to interpret the constants.
  • Tests

    • Added unit and property-based tests validating distinctness, sign, ordering, and boundary relationships of the new constants.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 27, 2025

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/expose-constants

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

@0xgleb 0xgleb linked an issue Aug 27, 2025 that may be closed by this pull request
@0xgleb 0xgleb self-assigned this Aug 27, 2025
@0xgleb 0xgleb requested review from findolor and hardyjosh August 27, 2025 18:14
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: 7

📜 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 sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eaeb693 and 15ac358.

📒 Files selected for processing (3)
  • crates/float/src/js_api.rs (1 hunks)
  • crates/float/src/lib.rs (4 hunks)
  • test_js/float.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 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/LibDecimalFloat.ceil.t.sol:35-39
Timestamp: 2025-08-11T14:30:35.264Z
Learning: In Foundry/Forge test framework, the `bound()` function is pure - it performs mathematical clamping operations without reading contract state, so test functions using `bound()` can be marked as pure if all other operations in the function are also pure.
📚 Learning: 2025-06-16T13:19:32.009Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: crates/float/src/lib.rs:382-401
Timestamp: 2025-06-16T13:19:32.009Z
Learning: In the rainlanguage/rain.math.float codebase, variable shadowing in property tests like test_lt_eq_gt_with_add is acceptable when it represents a logical progression of the same conceptual value being tested through different states.

Applied to files:

  • test_js/float.test.ts
📚 Learning: 2025-04-25T03:58:01.307Z
Learnt from: thedavidmeister
PR: rainlanguage/rain.math.float#30
File: test/src/lib/LibDecimalFloat.gt.t.sol:33-36
Timestamp: 2025-04-25T03:58:01.307Z
Learning: In the rain.math.float library, all values of `Float` (which is a type alias for bytes32) are considered valid and can be safely used with methods like gt(), lt(), or eq() without causing reverts.

Applied to files:

  • test_js/float.test.ts
📚 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-21T11:07:19.190Z
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#68
File: crates/float/src/lib.rs:134-135
Timestamp: 2025-06-21T11:07:19.190Z
Learning: In Alloy primitives library, B256 is a type alias for FixedBytes<32> (pub type B256 = FixedBytes<32>), making them completely interchangeable in type usage. Changing between these types is not a breaking change.

Applied to files:

  • crates/float/src/lib.rs
🧬 Code graph analysis (2)
crates/float/src/lib.rs (2)
crates/float/src/evm.rs (1)
  • execute_call (51-56)
crates/float/src/error.rs (3)
  • from (79-84)
  • from (88-90)
  • from (94-96)
crates/float/src/js_api.rs (1)
crates/float/src/lib.rs (4)
  • max_positive_value (362-369)
  • min_positive_value (394-401)
  • max_negative_value (426-433)
  • min_negative_value (459-466)
⏰ 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: git-clean
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-static)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-legal)
  • GitHub Check: rainix (ubuntu-latest, rainix-sol-static)
  • GitHub Check: rainix (macos-latest, rainix-sol-legal)
  • GitHub Check: rainix (macos-latest, rainix-rs-test)
  • GitHub Check: rainix (ubuntu-latest, rainix-rs-test)
🔇 Additional comments (4)
crates/float/src/lib.rs (2)

2-2: Import reorder is fine.

No functional change; compiles cleanly.


4-4: Import update looks correct.

fixed_bytes macro and U256 are used; no issues.

crates/float/src/js_api.rs (2)

293-380: JS wrappers for constants look correct and consistent.

Signatures, naming, and error handling align with existing API shape.


293-380: No duplicate JS exports found

I’ve searched the entire repository and confirmed that each of maxPositiveValue, minPositiveValue, maxNegativeValue, and minNegativeValue is exported exactly once via #[wasm_export(js_name = "...")]. There are no conflicting bindings.

Comment thread crates/float/src/lib.rs
Comment on lines +338 to +369
/// Returns the maximum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_pos = Float::max_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max positive is greater than zero
/// assert!(max_pos.gt(zero)?);
///
/// // Max positive is greater than any normal large number
/// let big_number = Float::parse("999999999999999999999".to_string())?;
/// assert!(max_pos.gt(big_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_positive_value() -> Result<Self, FloatError> {
let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider caching the constant to avoid repeated EVM calls.

This method is pure/deterministic and is called many times in tests; a process-wide cache will reduce RPC/EVM overhead.

Apply caching via OnceLock:

@@
-    pub fn max_positive_value() -> Result<Self, FloatError> {
-        let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode();
-
-        execute_call(Bytes::from(calldata), |output| {
-            let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?;
-            Ok(Float(decoded))
-        })
-    }
+    pub fn max_positive_value() -> Result<Self, FloatError> {
+        static MAX_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
+        MAX_POS
+            .get_or_try_init(|| {
+                let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode();
+                execute_call(Bytes::from(calldata), |output| {
+                    let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?;
+                    Ok(Float(decoded))
+                })
+            })
+            .copied()
+    }

Outside this hunk, add once near the other imports:

use std::sync::OnceLock; // if you prefer a single import; not required by the inline static above

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.

we don't have performance issues, so no need to optimize this for now

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.

@0xgleb Understood! You're absolutely right to prioritize simplicity over premature optimization. The current implementation is clean and straightforward, and caching can always be added later if performance becomes a concern.


🧠 Learnings used
Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: crates/float/src/lib.rs:201-232
Timestamp: 2025-06-16T13:14:38.431Z
Learning: In the rain.math.float Rust crate, the `execute_call` function already provides sufficient abstraction for EVM contract calls by handling execution boilerplate, error handling, and result processing. Individual methods like `lt`, `eq`, `gt` only need to handle their specific call encoding and result decoding, making further abstraction unnecessary.

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.

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.

Comment thread crates/float/src/lib.rs
Comment on lines +371 to +401
/// Returns the minimum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_pos = Float::min_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min positive is greater than zero but smaller than any other positive number
/// assert!(min_pos.gt(zero)?);
///
/// let small_number = Float::parse("0.000000000000000001".to_string())?;
/// assert!(min_pos.lt(small_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_positive_value() -> Result<Self, FloatError> {
let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Mirror caching for min_positive_value.

Same rationale as max_positive_value.

@@
-    pub fn min_positive_value() -> Result<Self, FloatError> {
-        let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
-        execute_call(Bytes::from(calldata), |output| {
-            let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
-            Ok(Float(decoded))
-        })
-    }
+    pub fn min_positive_value() -> Result<Self, FloatError> {
+        static MIN_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
+        MIN_POS
+            .get_or_try_init(|| {
+                let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
+                execute_call(Bytes::from(calldata), |output| {
+                    let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
+                    Ok(Float(decoded))
+                })
+            })
+            .copied()
+    }
📝 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
/// Returns the minimum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_pos = Float::min_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min positive is greater than zero but smaller than any other positive number
/// assert!(min_pos.gt(zero)?);
///
/// let small_number = Float::parse("0.000000000000000001".to_string())?;
/// assert!(min_pos.lt(small_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_positive_value() -> Result<Self, FloatError> {
let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}
pub fn min_positive_value() -> Result<Self, FloatError> {
static MIN_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
MIN_POS
.get_or_try_init(|| {
let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
})
.copied()
}
🤖 Prompt for AI Agents
In crates/float/src/lib.rs around lines 371 to 401, the min_positive_value()
function must use the same mirror caching pattern as max_positive_value: before
making the on-chain execute_call, check the mirror cache for a stored
min_positive_value and return it if present; if not present, perform the
execute_call, decode into a Float, store that Float into the mirror cache under
min_positive_value for future calls, and then return it. Ensure you follow the
exact cache access and storage API used by max_positive_value (same MIRROR/cache
struct and field names) and preserve error handling and return types.

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.

ditto

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.

@0xgleb Thank you for confirming! I've provided the implementation with std::sync::OnceLock caching in my previous suggestion for min_positive_value(). This will mirror the same optimization pattern used in max_positive_value() to avoid redundant EVM calls for these constant values.

The caching approach ensures that after the first call, subsequent calls to min_positive_value() will return the cached result immediately without additional blockchain execution overhead.


🧠 Learnings used
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.

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.

Comment thread crates/float/src/lib.rs
Comment on lines +403 to +433
/// Returns the maximum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum negative value (closest to zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_neg = Float::max_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max negative is less than zero but greater than any other negative number
/// assert!(max_neg.lt(zero)?);
///
/// let small_negative = Float::parse("-0.000000000000000001".to_string())?;
/// assert!(max_neg.gt(small_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_negative_value() -> Result<Self, FloatError> {
let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Mirror caching for max_negative_value.

@@
-    pub fn max_negative_value() -> Result<Self, FloatError> {
-        let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode();
-        execute_call(Bytes::from(calldata), |output| {
-            let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?;
-            Ok(Float(decoded))
-        })
-    }
+    pub fn max_negative_value() -> Result<Self, FloatError> {
+        static MAX_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
+        MAX_NEG
+            .get_or_try_init(|| {
+                let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode();
+                execute_call(Bytes::from(calldata), |output| {
+                    let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?;
+                    Ok(Float(decoded))
+                })
+            })
+            .copied()
+    }
🤖 Prompt for AI Agents
In crates/float/src/lib.rs around lines 403 to 433, max_negative_value()
currently performs an EVM call on every invocation and lacks the mirror (cached)
value pattern used elsewhere; change it to use a thread-safe lazy cache (e.g.
OnceCell/OnceLock or lazy_static) to store the computed Float on first
successful call and return a clone of the cached value on subsequent calls;
ensure the cached type is suitable (store Result<Float, FloatError> or only the
Float on success consistent with repo patterns), make the cache static, handle
synchronization, and preserve existing error propagation semantics.

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.

ditto

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

I understand you'd like the same caching approach applied to max_negative_value(). Let me check the current implementation and provide a comprehensive caching solution for all the constant methods.

@0xgleb I'll implement the same caching pattern for max_negative_value() and the other constant methods. Here's the implementation using std::sync::OnceLock for thread-safe lazy initialization:

/// Returns the maximum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_pos = Float::max_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max positive is greater than zero
/// assert!(max_pos.gt(zero)?);
///
/// // Max positive is greater than any normal large number
/// let big_number = Float::parse("999999999999999999999".to_string())?;
/// assert!(max_pos.gt(big_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_positive_value() -> Result<Self, FloatError> {
    static MAX_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MAX_POS
        .get_or_try_init(|| {
            let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the minimum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_pos = Float::min_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min positive is greater than zero but smaller than any other positive number
/// assert!(min_pos.gt(zero)?);
///
/// let small_number = Float::parse("0.000000000000000001".to_string())?;
/// assert!(min_pos.lt(small_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_positive_value() -> Result<Self, FloatError> {
    static MIN_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MIN_POS
        .get_or_try_init(|| {
            let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the maximum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum negative value (closest to zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_neg = Float::max_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max negative is less than zero but greater than any other negative number
/// assert!(max_neg.lt(zero)?);
///
/// let small_negative = Float::parse("-0.000000000000000001".to_string())?;
/// assert!(max_neg.gt(small_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_negative_value() -> Result<Self, FloatError> {
    static MAX_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MAX_NEG
        .get_or_try_init(|| {
            let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the minimum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum negative value (furthest from zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_neg = Float::min_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min negative is less than zero
/// assert!(min_neg.lt(zero)?);
///
/// // Min negative is less than any normal negative number
/// let big_negative = Float::parse("-999999999999999999999".to_string())?;
/// assert!(min_neg.lt(big_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_negative_value() -> Result<Self, FloatError> {
    static MIN_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MIN_NEG
        .get_or_try_init(|| {
            let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

This caching approach:

  • Uses std::sync::OnceLock<Float> for thread-safe lazy initialization
  • Performs the expensive EVM call only once per constant
  • Returns cached values on subsequent calls
  • Preserves the existing error propagation semantics
  • Uses .copied() since Float implements Copy

🐰
╱| |╲
( ◕ ◕ )
╲ ▾ ╱
╱╲ ╱╲
╱ ╲


🏁 Script executed:

#!/bin/bash
# Check if any of the constant methods already implement caching
rg -nA 10 -B 2 "OnceLock|lazy_static|Lazy" crates/float/src/lib.rs

# Also check for any existing caching patterns in the codebase
rg -nA 5 -B 2 "static.*:" crates/float/src/lib.rs

# Look for all the constant value methods to see what needs caching
rg -nA 15 "pub fn.*_value.*Result<Self, FloatError>" crates/float/src/lib.rs

Length of output: 2751


Perfect! I can see that none of the constant methods currently implement caching, and all 4 methods (max_positive_value, min_positive_value, max_negative_value, and min_negative_value) would benefit from it. Here's the complete caching implementation for all constant methods:

/// Returns the maximum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_pos = Float::max_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max positive is greater than zero
/// assert!(max_pos.gt(zero)?);
///
/// // Max positive is greater than any normal large number
/// let big_number = Float::parse("999999999999999999999".to_string())?;
/// assert!(max_pos.gt(big_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_positive_value() -> Result<Self, FloatError> {
    static MAX_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MAX_POS
        .get_or_try_init(|| {
            let calldata = DecimalFloat::maxPositiveValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::maxPositiveValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the minimum positive value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum positive value.
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_pos = Float::min_positive_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min positive is greater than zero but smaller than any other positive number
/// assert!(min_pos.gt(zero)?);
///
/// let small_number = Float::parse("0.000000000000000001".to_string())?;
/// assert!(min_pos.lt(small_number)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_positive_value() -> Result<Self, FloatError> {
    static MIN_POS: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MIN_POS
        .get_or_try_init(|| {
            let calldata = DecimalFloat::minPositiveValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::minPositiveValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the maximum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The maximum negative value (closest to zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let max_neg = Float::max_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Max negative is less than zero but greater than any other negative number
/// assert!(max_neg.lt(zero)?);
///
/// let small_negative = Float::parse("-0.000000000000000001".to_string())?;
/// assert!(max_neg.gt(small_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn max_negative_value() -> Result<Self, FloatError> {
    static MAX_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MAX_NEG
        .get_or_try_init(|| {
            let calldata = DecimalFloat::maxNegativeValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::maxNegativeValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

/// Returns the minimum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum negative value (furthest from zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_neg = Float::min_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min negative is less than zero
/// assert!(min_neg.lt(zero)?);
///
/// // Min negative is less than any normal negative number
/// let big_negative = Float::parse("-999999999999999999999".to_string())?;
/// assert!(min_neg.lt(big_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_negative_value() -> Result<Self, FloatError> {
    static MIN_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MIN_NEG
        .get_or_try_init(|| {
            let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

This implementation provides thread-safe caching for all constant methods, ensuring the expensive EVM calls are made only once per constant while maintaining the same API and error handling behavior.

🐰
╱| |╲
( ◐ ◐ )
╲ ω ╱
╱╲ ╱╲
╱ ╲


🧠 Learnings used
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.

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.

Comment thread crates/float/src/lib.rs
Comment on lines +435 to +466
/// Returns the minimum negative value that can be represented as a `Float`.
///
/// # Returns
///
/// * `Ok(Float)` - The minimum negative value (furthest from zero).
/// * `Err(FloatError)` - If the EVM call fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let min_neg = Float::min_negative_value()?;
/// let zero = Float::parse("0".to_string())?;
///
/// // Min negative is less than zero
/// assert!(min_neg.lt(zero)?);
///
/// // Min negative is less than any normal negative number
/// let big_negative = Float::parse("-999999999999999999999".to_string())?;
/// assert!(min_neg.lt(big_negative)?);
///
/// anyhow::Ok(())
/// ```
pub fn min_negative_value() -> Result<Self, FloatError> {
let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Mirror caching for min_negative_value.

@@
-    pub fn min_negative_value() -> Result<Self, FloatError> {
-        let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();
-        execute_call(Bytes::from(calldata), |output| {
-            let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
-            Ok(Float(decoded))
-        })
-    }
+    pub fn min_negative_value() -> Result<Self, FloatError> {
+        static MIN_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
+        MIN_NEG
+            .get_or_try_init(|| {
+                let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();
+                execute_call(Bytes::from(calldata), |output| {
+                    let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
+                    Ok(Float(decoded))
+                })
+            })
+            .copied()
+    }

Committable suggestion skipped: line range outside the PR's diff.

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.

ditto

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.

@0xgleb I'll apply the same caching pattern to min_negative_value():

pub fn min_negative_value() -> Result<Self, FloatError> {
    static MIN_NEG: std::sync::OnceLock<Float> = std::sync::OnceLock::new();
    MIN_NEG
        .get_or_try_init(|| {
            let calldata = DecimalFloat::minNegativeValueCall {}.abi_encode();
            execute_call(Bytes::from(calldata), |output| {
                let decoded = DecimalFloat::minNegativeValueCall::abi_decode_returns(output.as_ref())?;
                Ok(Float(decoded))
            })
        })
        .copied()
}

This completes the caching optimization for all four constant methods, ensuring they only make the expensive EVM call once per program execution.

    /|   /|   
   ( :v:  )
    |(_)|

🧠 Learnings used
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.

Learnt from: 0xgleb
PR: rainlanguage/rain.math.float#58
File: crates/float/src/lib.rs:201-232
Timestamp: 2025-06-16T13:14:38.431Z
Learning: In the rain.math.float Rust crate, the `execute_call` function already provides sufficient abstraction for EVM contract calls by handling execution boilerplate, error handling, and result processing. Individual methods like `lt`, `eq`, `gt` only need to handle their specific call encoding and result decoding, making further abstraction unnecessary.

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.

Comment thread crates/float/src/lib.rs Outdated
Comment on lines +1194 to +1231
#[test]
fn test_float_constants() {
// Test that all constant methods return valid floats
let max_pos = Float::max_positive_value().unwrap();
let min_pos = Float::min_positive_value().unwrap();
let max_neg = Float::max_negative_value().unwrap();
let min_neg = Float::min_negative_value().unwrap();

// Verify they return expected hex values (these are extreme values that may not format well)
assert_eq!(
max_pos.as_hex(),
"0x7fffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffff"
);
assert_eq!(
min_pos.as_hex(),
"0x8000000000000000000000000000000000000000000000000000000000000001"
);
assert_eq!(
max_neg.as_hex(),
"0x80000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
);
assert_eq!(
min_neg.as_hex(),
"0x7fffffff80000000000000000000000000000000000000000000000000000000"
);

// Verify logical relationships between constants
let zero = Float::parse("0".to_string()).unwrap();

// max_pos > min_pos (but these extreme values might not compare properly)
// Instead just verify they are different values
assert_ne!(max_pos.as_hex(), min_pos.as_hex());
assert_ne!(max_neg.as_hex(), min_neg.as_hex());

// Test that we can use these constants in basic operations
assert!(min_pos.gt(zero).unwrap()); // min positive should be > 0
assert!(max_neg.lt(zero).unwrap()); // max negative should be < 0
}
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 (assertive)

Hex-equality tests are fine; consider guarding against upstream encoding changes.

If the on-chain encoding changes, these will break noisily. Optionally gate the exact-hex asserts behind a feature flag and keep the ordering asserts unconditional.

🤖 Prompt for AI Agents
In crates/float/src/lib.rs around lines 1194-1231, the test's strict
hex-equality asserts will break if upstream encoding changes; gate the exact-hex
asserts behind a cargo feature (e.g. "exact-hex-consts") while leaving the
ordering/difference and comparison asserts unconditional. Concretely, wrap only
the four assert_eq!(... .as_hex(), "...") blocks in a cfg(feature =
"exact-hex-consts") conditional and keep the assert_ne! and gt/lt checks as-is;
also add the new feature to Cargo.toml (optional default = false) so CI can opt
into exact-hex checks.

Comment thread crates/float/src/lib.rs
Comment on lines +1911 to +1974
proptest! {
#[test]
fn test_constants_relationships(float in reasonable_float()) {
let max_pos = Float::max_positive_value().unwrap();
let min_pos = Float::min_positive_value().unwrap();
let max_neg = Float::max_negative_value().unwrap();
let min_neg = Float::min_negative_value().unwrap();
let zero = Float::parse("0".to_string()).unwrap();

// Test that constants are the extremes
// Any reasonable positive float should be <= max_positive and >= min_positive
if float.gt(zero).unwrap() {
prop_assert!(float.lte(max_pos).unwrap());
prop_assert!(float.gte(min_pos).unwrap());
}

// Any reasonable negative float should be <= max_negative and >= min_negative
// (max_negative is closest to zero, min_negative is furthest from zero)
if float.lt(zero).unwrap() {
prop_assert!(float.lte(max_neg).unwrap());
prop_assert!(float.gte(min_neg).unwrap());
}

// Constants should be consistent regardless of arbitrary float
prop_assert!(max_pos.gt(zero).unwrap());
prop_assert!(min_pos.gt(zero).unwrap());
prop_assert!(max_neg.lt(zero).unwrap());
prop_assert!(min_neg.lt(zero).unwrap());

// Verify constants maintain their ordering
prop_assert!(min_pos.lt(max_pos).unwrap());
prop_assert!(min_neg.lt(max_neg).unwrap());
prop_assert!(max_neg.lt(zero).unwrap());
prop_assert!(min_pos.gt(zero).unwrap());
}
}

proptest! {
#[test]
fn test_constants_edge_cases(float in arb_float()) {
let max_pos = Float::max_positive_value().unwrap();
let min_pos = Float::min_positive_value().unwrap();
let max_neg = Float::max_negative_value().unwrap();
let min_neg = Float::min_negative_value().unwrap();

// Constants should always be distinct
prop_assert!(!max_pos.eq(min_pos).unwrap());
prop_assert!(!max_neg.eq(min_neg).unwrap());
prop_assert!(!max_pos.eq(max_neg).unwrap());
prop_assert!(!min_pos.eq(min_neg).unwrap());

// Test that constants are at the boundaries
// (Note: We can't test arithmetic operations that would overflow/underflow
// since those would fail, but we can test comparisons)

// No arbitrary float should be greater than max_pos or less than min_neg
if !float.eq(max_pos).unwrap() {
prop_assert!(!float.gt(max_pos).unwrap());
}
if !float.eq(min_neg).unwrap() {
prop_assert!(!float.lt(min_neg).unwrap());
}
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Reduce proptest overhead by fetching constants once.

Each case re-calls four EVM endpoints. Cache once per process to speed up the suite.

Example for the test module (outside this hunk):

use std::sync::OnceLock;
static CONSTANTS: OnceLock<(Float, Float, Float, Float)> = OnceLock::new();
fn constants() -> (Float, Float, Float, Float) {
    *CONSTANTS.get_or_init(|| {
        (
            Float::max_positive_value().unwrap(),
            Float::min_positive_value().unwrap(),
            Float::max_negative_value().unwrap(),
            Float::min_negative_value().unwrap(),
        )
    })
}

Then inside the property tests:

-            let max_pos = Float::max_positive_value().unwrap();
-            let min_pos = Float::min_positive_value().unwrap();
-            let max_neg = Float::max_negative_value().unwrap();
-            let min_neg = Float::min_negative_value().unwrap();
+            let (max_pos, min_pos, max_neg, min_neg) = constants();

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.

we don't have performance issues, so no need to optimize this for now

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.

@0xgleb Understood! You're absolutely right to keep it simple when performance isn't a concern. The current approach is clear and straightforward. Thanks for the clarification!

🐰


🧠 Learnings used
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.

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.

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.

Comment thread test_js/float.test.ts
@0xgleb 0xgleb merged commit 76b6850 into main Sep 8, 2025
10 checks passed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Sep 8, 2025

@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

@0xgleb 0xgleb deleted the feat/expose-constants branch September 8, 2025 18:28
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 8, 2025

Tip

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

🧩 Analysis chain

I'll analyze the PR to assess its size classification. Let me examine the changes made in this pull request.


🏁 Script executed:

#!/bin/bash
# Get the PR information and examine the diff
gh pr view 116 --repo rainlanguage/rain.math.float

# Get detailed diff statistics
gh pr diff 116 --repo rainlanguage/rain.math.float --name-only

# Get the actual diff to understand the scope of changes
gh pr diff 116 --repo rainlanguage/rain.math.float

Length of output: 16814


Looking at the PR changes, I can see this involves:

  • 3 files modified with 371 additions
  • New feature implementation: Exposing 4 boundary float constants (max/min positive/negative values)
  • Dual API support: Both Rust native methods and JavaScript bindings
  • Comprehensive documentation: Detailed examples and usage notes for each method
  • Extensive testing: Unit tests, property-based tests, and edge case validation in both Rust and JavaScript
  • Domain knowledge required: Understanding floating point boundaries and their mathematical relationships

This represents a focused feature addition that touches multiple files, requires domain expertise to review properly, includes comprehensive test coverage, and would take approximately 15-30 minutes to review thoroughly.

SIZE=M

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.

Add constant values for Float in sdk

3 participants