Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 212 additions & 208 deletions .gas-snapshot

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .github/workflows/manual-sol-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ jobs:
DEPLOY_BROADCAST: '1'
DEPLOYMENT_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRIVATE_KEY || secrets.PRIVATE_KEY_DEV }}
ETH_RPC_URL: ${{ secrets[env.rpc_secret_name] || vars[env.rpc_secret_name] || '' }}
CI_FORK_ETH_RPC_URL: ${{ secrets.RPC_URL_ETHEREUM_FORK || vars.RPC_URL_ETHEREUM_FORK || '' }}
CI_FORK_FLARE_RPC_URL: ${{ secrets.RPC_URL_FLARE_FORK || vars.RPC_URL_FLARE_FORK || '' }}
Comment on lines +75 to +76
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'CI_FORK_(ETH|FLARE)_RPC_URL' --type yaml --type ts --type js --type sh

Repository: rainlanguage/rain.math.float

Length of output: 54


🏁 Script executed:

# Search more broadly for these env vars
rg -n 'CI_FORK_ETH_RPC_URL|CI_FORK_FLARE_RPC_URL'

Repository: rainlanguage/rain.math.float

Length of output: 289


🏁 Script executed:

# Also search for how RPC_URL is typically used in this repo
rg -n 'RPC.*URL' .github/workflows/ | head -30

Repository: rainlanguage/rain.math.float

Length of output: 912


🏁 Script executed:

cat -n test/src/lib/deploy/LibDecimalFloatDeploy.t.sol

Repository: rainlanguage/rain.math.float

Length of output: 1687


Tests will fail when fork RPC URLs are not configured. The env vars are set unconditionally with || '' fallback, causing vm.createSelectFork() calls to receive empty strings when secrets/vars are undefined. The tests at test/src/lib/deploy/LibDecimalFloatDeploy.t.sol:11 and line 28 will fail since Foundry requires non-empty RPC URLs.

Either remove the || '' fallback and let tests skip gracefully, or add conditional logic in tests to skip when these env vars are empty.

🤖 Prompt for AI Agents
In @.github/workflows/manual-sol-artifacts.yaml around lines 75 - 76, The
workflow currently forces CI_FORK_ETH_RPC_URL and CI_FORK_FLARE_RPC_URL to an
empty string via "|| ''", which passes empty values into vm.createSelectFork()
and makes Foundry tests fail; remove the "|| ''" fallbacks from the
CI_FORK_ETH_RPC_URL and CI_FORK_FLARE_RPC_URL assignments so the env vars are
truly undefined when not set, or alternatively add a guard in the Solidity tests
(e.g., in LibDecimalFloatDeploy.t.sol around the vm.createSelectFork() calls) to
detect an empty/undefined env var and skip the test instead; update either the
.github/workflows/manual-sol-artifacts.yaml lines containing CI_FORK_ETH_RPC_URL
/ CI_FORK_FLARE_RPC_URL or add the conditional skip logic in the test functions
that call vm.createSelectFork().

ETHERSCAN_API_KEY: ${{ secrets[env.etherscan_api_key_secret_name] || vars[env.etherscan_api_key_secret_name] || ''}}
DEPLOY_VERIFY: ${{ secrets[env.verify_secret_name] || vars[env.verify_secret_name] || '' }}
DEPLOY_VERIFIER: ${{ secrets[env.verifier_secret_name] || vars[env.verifier_secret_name] || '' }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/rainix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jobs:
- name: Run ${{ matrix.task }}
env:
ETH_RPC_URL: ${{ secrets.CI_DEPLOY_SEPOLIA_RPC_URL || vars.CI_DEPLOY_SEPOLIA_RPC_URL }}
CI_FORK_ARB_RPC_URL: ${{ secrets.RPC_URL_ARBITRUM_FORK || vars.RPC_URL_ARBITRUM_FORK || '' }}
CI_FORK_ETH_RPC_URL: ${{ secrets.RPC_URL_ETHEREUM_FORK || vars.RPC_URL_ETHEREUM_FORK || '' }}
CI_FORK_BASE_RPC_URL: ${{ secrets.RPC_URL_BASE_FORK || vars.RPC_URL_BASE_FORK || '' }}
CI_FORK_FLARE_RPC_URL: ${{ secrets.RPC_URL_FLARE_FORK || vars.RPC_URL_FLARE_FORK || '' }}
Comment on lines 55 to 60
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "rainix.yaml" -type f 2>/dev/null | head -5

Repository: rainlanguage/rain.math.float

Length of output: 54


🏁 Script executed:

find . -path "*/.github/workflows/rainix.yaml" -type f

Repository: rainlanguage/rain.math.float

Length of output: 103


🏁 Script executed:

git ls-files | grep -E "(rainix|workflows)" | head -20

Repository: rainlanguage/rain.math.float

Length of output: 258


🏁 Script executed:

cat -n .github/workflows/rainix.yaml | sed -n '45,70p'

Repository: rainlanguage/rain.math.float

Length of output: 1583


🏁 Script executed:

rg -n "CI_FORK_ETH_RPC_URL|CI_FORK_FLARE_RPC_URL" --type bash --type shell --type nix --type rust --type ts --type js

Repository: rainlanguage/rain.math.float

Length of output: 100


🏁 Script executed:

rg -rn "CI_FORK_ETH_RPC_URL|CI_FORK_FLARE_RPC_URL"

Repository: rainlanguage/rain.math.float

Length of output: 245


🏁 Script executed:

fd -e nix | head -20

Repository: rainlanguage/rain.math.float

Length of output: 81


🏁 Script executed:

git ls-files | grep -E "\.(nix|rs|ts|js|sh)$" | grep -v node_modules | head -30

Repository: rainlanguage/rain.math.float

Length of output: 243


🏁 Script executed:

rg "CI_FORK" -A 2 -B 2

Repository: rainlanguage/rain.math.float

Length of output: 1204


🏁 Script executed:

cat -n flake.nix | head -100

Repository: rainlanguage/rain.math.float

Length of output: 1520


🏁 Script executed:

git ls-files | grep -E "test|spec" | grep -E "\.(sol|ts|js|sh|nix)$" | head -20

Repository: rainlanguage/rain.math.float

Length of output: 930


🏁 Script executed:

cat -n test/src/lib/deploy/LibDecimalFloatDeploy.t.sol

Repository: rainlanguage/rain.math.float

Length of output: 1687


🏁 Script executed:

rg "vm.envString|vm.envOpt" -A 2 -B 2

Repository: rainlanguage/rain.math.float

Length of output: 940


🏁 Script executed:

cat -n test/src/lib/deploy/LibDecimalFloatDeploy.t.sol | sed -n '10,36p'

Repository: rainlanguage/rain.math.float

Length of output: 1284


🌐 Web query:

Forge vm.envString() behavior empty string vs unset environment variable

💡 Result:

In Foundry/Forge cheatcodes, vm.envString("KEY") distinguishes unset vs set-to-empty like this:

  • Unset / not defined env varvm.envString(key) reverts with “environment variable not found”. [1]
  • Defined but empty (e.g. KEY= in .env, or vm.setEnv("KEY", "")) → vm.envString(key) returns the empty string ("") and does not revert (empty values are allowed by setEnv; only NUL is disallowed). [2]

If you want “unset → default” (instead of reverting), use vm.envOr(...)-style helpers (e.g., envOr) rather than envString. [3]

Sources: [1] [2] [3]


Guard against empty fork RPC URLs to prevent failures with empty strings.

Tests use vm.envString() which reverts if unset but returns empty string if set-to-empty. With the || '' fallback, CI_FORK_ETH_RPC_URL and CI_FORK_FLARE_RPC_URL become set-to-empty, causing vm.createSelectFork("") to fail with a confusing error instead of reverting early. Unset empty values to preserve the "var not found" semantics and allow tests to skip gracefully.

🔧 Suggested guard to unset empty values before running
-        run: nix develop -c ${{ matrix.task }}
+        run: |
+          if [ -z "$CI_FORK_ETH_RPC_URL" ]; then unset CI_FORK_ETH_RPC_URL; fi
+          if [ -z "$CI_FORK_FLARE_RPC_URL" ]; then unset CI_FORK_FLARE_RPC_URL; fi
+          nix develop -c ${{ matrix.task }}
🤖 Prompt for AI Agents
In @.github/workflows/rainix.yaml around lines 55 - 58, The workflow sets
CI_FORK_ETH_RPC_URL and CI_FORK_FLARE_RPC_URL to an empty string via the "|| ''"
fallback which causes vm.envString() to return an empty value and later makes
vm.createSelectFork("") fail; remove the "|| ''" fallback so the env vars are
left unset when secrets/vars are absent (or add a conditional step that unsets
the variable when its resolved value is empty) — update the two lines that
assign CI_FORK_ETH_RPC_URL and CI_FORK_FLARE_RPC_URL in the workflow to omit the
empty-string fallback so tests preserve the "var not found" behavior used by
vm.envString()/vm.createSelectFork.

ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY }}
DEPLOY_BROADCAST: ""
DEPLOY_VERIFIER: ""
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ target
temp
dist
node_modules
.env
94 changes: 84 additions & 10 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,38 @@ impl Div for Float {
}

impl Float {
/// Returns the integer part of the float (truncation toward zero).
///
/// # Returns
///
/// * `Ok(Float)` - The integer part.
/// * `Err(FloatError)` - If the operation fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let x = Float::parse("3.75".to_string())?;
/// let int = x.integer()?;
/// assert_eq!(int.format()?, "3");
///
/// let y = Float::parse("-3.75".to_string())?;
/// let int_y = y.integer()?;
/// assert_eq!(int_y.format()?, "-3");
///
/// anyhow::Ok(())
/// ```
pub fn integer(self) -> Result<Float, FloatError> {
let Float(a) = self;
let calldata = DecimalFloat::integerCall { a }.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::integerCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}

/// Returns the fractional part of the float.
///
/// # Returns
Expand Down Expand Up @@ -1804,6 +1836,48 @@ mod tests {
assert!(frac.eq(expected_frac).unwrap());
}

#[test]
fn test_integer_positive() {
let float = Float::parse("12345.6789".to_string()).unwrap();
let int = float.integer().unwrap();
let expected = Float::parse("12345".to_string()).unwrap();
assert!(int.eq(expected).unwrap());

let frac = float.frac().unwrap();
let recombined = (int + frac).unwrap();
assert!(float.eq(recombined).unwrap());
}

#[test]
fn test_integer_negative() {
let float = Float::parse("-12345.6789".to_string()).unwrap();
let int = float.integer().unwrap();
let frac = float.frac().unwrap();

// integer truncates toward zero, so -12345.6789 -> -12345
let expected_int = Float::parse("-12345".to_string()).unwrap();
let expected_frac = Float::parse("-0.6789".to_string()).unwrap();

assert!(int.eq(expected_int).unwrap());
assert!(frac.eq(expected_frac).unwrap());

// integer + frac == original
let recombined = (int + frac).unwrap();
assert!(float.eq(recombined).unwrap());
}

#[test]
fn test_integer_whole_numbers() {
let pos = Float::parse("42".to_string()).unwrap();
assert!(pos.integer().unwrap().eq(pos).unwrap());
let zero = Float::parse("0".to_string()).unwrap();
assert!(pos.frac().unwrap().eq(zero).unwrap());

let neg = Float::parse("-42".to_string()).unwrap();
assert!(neg.integer().unwrap().eq(neg).unwrap());
assert!(neg.frac().unwrap().eq(zero).unwrap());
}

proptest! {
#[test]
fn test_from_to_fixed_decimal_valid_range(coeff in any::<I224>(), decimals in 0u8..=66u8) {
Expand All @@ -1823,30 +1897,30 @@ mod tests {

proptest! {
#[test]
fn test_frac_floor_properties(float in arb_float()) {
let floor = float.floor().unwrap();
fn test_int_frac_properties(float in arb_float()) {
let int = float.integer().unwrap();
let frac = float.frac().unwrap();

let zero = Float::parse("0".to_string()).unwrap();

prop_assert!(
floor.frac().unwrap().eq(zero).unwrap(),
"floor.frac() is not zero: {}",
floor.show_unpacked().unwrap()
int.frac().unwrap().eq(zero).unwrap(),
"int.frac() is not zero: {}",
int.show_unpacked().unwrap()
);

prop_assert!(
frac.floor().unwrap().eq(zero).unwrap(),
"frac.floor() is not zero: {}",
frac.integer().unwrap().eq(zero).unwrap(),
"frac.integer() is not zero: {}",
frac.show_unpacked().unwrap()
);

let recombined = (floor + frac).unwrap();
let recombined = (int + frac).unwrap();
prop_assert!(
float.eq(recombined).unwrap(),
"original: {}, floor: {}, frac: {}, recombined: {}",
"original: {}, int: {}, frac: {}, recombined: {}",
float.show_unpacked().unwrap(),
floor.show_unpacked().unwrap(),
int.show_unpacked().unwrap(),
frac.show_unpacked().unwrap(),
recombined.show_unpacked().unwrap()
);
Expand Down
59 changes: 38 additions & 21 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"lib/forge-std": {
"rev": "b8f065fda83b8cd94a6b2fec8fcd911dc3b444fd"
"rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6"
},
"lib/rain.datacontract": {
"rev": "061bf7cd63496ccbe4f74bcd0c48715883c1c3cb"
"rev": "82590300cf768b6c5efc8b92c64b7f402bd34bee"
},
"lib/rain.math.fixedpoint": {
"rev": "1752e9fbd635901ac7eb177699681ed97290e12e"
"rev": "8308cbb6da0e231c6f3437f1861e66eff7ea2b00"
},
"lib/rain.sol.codegen": {
"rev": "bd7993b3f6b301e5a667ff687f25b80fdda878cd"
},
"lib/rain.string": {
"rev": "0b1ca08aed6d9c06b83fe127a7d20ee7002ead28"
"rev": "488f237cd59874e4eb91b5a4f747bd57578fec7f"
}
}
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 57 files
+14 −31 .github/workflows/ci.yml
+1 −1 .github/workflows/sync.yml
+2 −2 CONTRIBUTING.md
+11 −9 README.md
+3 −12 foundry.toml
+2 −2 package.json
+2 −12 scripts/vm.py
+2 −2 src/Base.sol
+1 −1 src/Config.sol
+2 −2 src/LibVariable.sol
+2 −2 src/Script.sol
+28 −13 src/StdAssertions.sol
+8 −5 src/StdChains.sol
+9 −13 src/StdCheats.sol
+24 −4 src/StdConfig.sol
+2 −2 src/StdConstants.sol
+2 −2 src/StdError.sol
+2 −4 src/StdInvariant.sol
+4 −12 src/StdJson.sol
+2 −2 src/StdMath.sol
+11 −9 src/StdStorage.sol
+2 −2 src/StdStyle.sol
+4 −12 src/StdToml.sol
+5 −13 src/StdUtils.sol
+2 −4 src/Test.sol
+26 −41 src/Vm.sol
+10 −19 src/console.sol
+2 −2 src/console2.sol
+2 −2 src/interfaces/IERC1155.sol
+2 −2 src/interfaces/IERC165.sol
+2 −2 src/interfaces/IERC20.sol
+2 −2 src/interfaces/IERC4626.sol
+2 −2 src/interfaces/IERC6909.sol
+2 −2 src/interfaces/IERC721.sol
+4 −10 src/interfaces/IERC7540.sol
+2 −2 src/interfaces/IERC7575.sol
+3 −8 src/interfaces/IMulticall3.sol
+691 −1,380 src/safeconsole.sol
+2 −2 test/CommonBase.t.sol
+34 −5 test/Config.t.sol
+19 −1 test/LibVariable.t.sol
+2 −2 test/StdAssertions.t.sol
+24 −24 test/StdChains.t.sol
+19 −20 test/StdCheats.t.sol
+2 −2 test/StdConstants.t.sol
+3 −4 test/StdError.t.sol
+2 −2 test/StdJson.t.sol
+6 −6 test/StdMath.t.sol
+24 −27 test/StdStorage.t.sol
+2 −2 test/StdStyle.t.sol
+2 −2 test/StdToml.t.sol
+13 −13 test/StdUtils.t.sol
+3 −3 test/Vm.t.sol
+2 −4 test/compilation/CompilationScript.sol
+2 −4 test/compilation/CompilationScriptBase.sol
+2 −4 test/compilation/CompilationTest.sol
+2 −4 test/compilation/CompilationTestBase.sol
14 changes: 12 additions & 2 deletions src/concrete/DecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,18 @@ contract DecimalFloat {
}

/// Exposes `LibFormatDecimalFloat.toDecimalString` for offchain use.
/// @param a The float to format.
/// @param a The float to format. The absolute value of `a` is used to
/// determine if scientific notation is used, this allows negative numbers to
/// be formatted consistently with their positive counterparts.
/// @param scientificMin The smallest number that won't be formatted in
/// scientific notation.
/// @param scientificMax The largest number that won't be formatted in
/// scientific notation.
/// @return The string representation of the float.
function format(Float a, Float scientificMin, Float scientificMax) public pure returns (string memory) {
require(scientificMin.lt(scientificMax), "scientificMin must be less than scientificMax");
return LibFormatDecimalFloat.toDecimalString(a, a.lt(scientificMin) || a.gt(scientificMax));
Float absA = a.abs();
return LibFormatDecimalFloat.toDecimalString(a, absA.lt(scientificMin) || absA.gt(scientificMax));
}

/// Exposes `LibFormatDecimalFloat.toDecimalString` for offchain use.
Expand Down Expand Up @@ -191,6 +194,13 @@ contract DecimalFloat {
return a.gte(b);
}

/// Exposes `LibDecimalFloat.integer` for offchain use.
/// @param a The float to get the integer part of.
/// @return The integer part of the float.
function integer(Float a) external pure returns (Float) {
return a.integer();
}

/// Exposes `LibDecimalFloat.frac` for offchain use.
/// @param a The float to get the fractional part of.
/// @return The fractional part of the float.
Expand Down
3 changes: 3 additions & 0 deletions src/error/ErrDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ error DivisionByZero(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when attempting to exponentiate a negative base.
error PowNegativeBase(int256 signedCoefficient, int256 exponent);

/// @dev Thrown if writing the data by creating the contract fails somehow.
error WriteError();
Loading