Skip to content

Floats.#23

Open
AStepanov25 wants to merge 2 commits into
mainfrom
float
Open

Floats.#23
AStepanov25 wants to merge 2 commits into
mainfrom
float

Conversation

@AStepanov25
Copy link
Copy Markdown
Contributor

@AStepanov25 AStepanov25 commented Jan 26, 2026

Summary by CodeRabbit

  • New Features

    • Added a public float decomposition utility and a generic sort signature for numeric keys.
  • Tests

    • Added a comprehensive test suite validating decomposition across integers, decimals, small magnitudes, powers of ten, precision/overflow cases, and special float values (zero, infinities, NaN).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Adds a new src/FloatKey.mo Motoko module with a public Decomposer(maxSignificantDigits : Nat) class exposing decompose(price : Float) : ?(Nat32, Int), plus a generic sort signature. Adds test/float.test.mo which constructs FloatKey.Decomposer(5) and runs a test suite validating decomposition outputs across many float cases.

Changes

Cohort / File(s) Summary
Float decomposition implementation
src/FloatKey.mo
Introduces module containing Decomposer(maxSignificantDigits : Nat) (validates digits, precomputes constants) and its decompose(price : Float) : ?(Nat32, Int) implementation; returns null for NaN/<=0 and otherwise (Nat32, Int) on success. Also adds a public generic sort<T>(...) signature (empty body).
Tests for decomposer
test/float.test.mo
Adds test module that imports FloatKey, builds let d = FloatKey.Decomposer(5), defines assertEqual and test_suite, and exercises d.decompose across integers, decimals, powers of ten, sub-unit values, precision/overflow cases, and special floats (0, ±inf, NaN).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble digits, hop and play,
I split floats into bins of day,
Five sigs guide my tiny art,
Exponent, mantissa — a rabbit's part 🐇📐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Floats.' is overly vague and generic, providing no meaningful information about the specific changes introduced. Replace with a descriptive title that clearly indicates the main change, such as 'Add FloatKey decomposer module with float normalization' or 'Implement float decomposition and sorting functionality'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch float

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@test/float.test.mo`:
- Around line 11-33: In decompose_Original, use a single precise ln(10) constant
instead of the two differing literals and add handling for non-positive inputs:
define a single constant (e.g., ln10) used in both branches (replace occurrences
of 2.302_585_092_994_045 and 2.302_585_092_994_047), and at the start of
decompose_Original guard against price <= 0 by returning null (or an appropriate
sentinel) to avoid Float.log(0) and Float.log(negative) producing -infinity/NaN;
update references inside the function to use ln10 and ensure the early-return
covers zero and negative prices before any Float.log calls.
🧹 Nitpick comments (3)
test/float.test.mo (3)

38-60: Clarify negative price handling behavior.

The comment on line 41 mentions handling negative prices "safely" with abs(), but the current behavior silently converts negative prices to positive results via Int.abs(Float.toInt(r)) on line 56. This means -123.45 would return ?(12345, -2) — the same as 123.45.

If negative prices should be rejected, explicitly return null. If they should be supported, consider how sign information should be preserved or documented.

Option A: Reject negative prices
 func decompose_Improved(price : Float) : ?(Nat32, Int) {
   if (price == 0) return ?(0, 0);
+  if (price < 0) return null;

   // Uses abs() to handle potential negative prices safely
-  let logPrice = Float.log(Float.abs(price)) / 2.302_585_092_994_045;
+  let logPrice = Float.log(price) / 2.302_585_092_994_045;
Option B: Document the behavior
-  // Uses abs() to handle potential negative prices safely
+  // Negative prices are treated as their absolute value (sign is discarded)
   let logPrice = Float.log(Float.abs(price)) / 2.302_585_092_994_045;

66-80: Consider adding assertion traps for CI integration.

The current implementation prints pass/fail results but doesn't actually fail the test execution on assertion failures. In a CI/CD pipeline, test failures would be silent (process exits successfully).

Add trap on failure for CI
 func assertEqual(name : Text, actual : ?(Nat32, Int), expected : ?(Nat32, Int)) {
   switch (actual, expected) {
     case (null, null) { Debug.print("✅ " # name # ": PASS (Both null)") };
     case (?(a), ?(b)) {
       if (a.0 == b.0 and a.1 == b.1) {
         Debug.print("✅ " # name # ": PASS (" # debug_show (a) # ")");
       } else {
-        Debug.print("❌ " # name # ": FAIL. Got " # debug_show (a) # ", Expected " # debug_show (b));
+        Debug.trap("❌ " # name # ": FAIL. Got " # debug_show (a) # ", Expected " # debug_show (b));
       };
     };
     case (_, _) {
-      Debug.print("❌ " # name # ": FAIL. Mismatch null vs value.");
+      Debug.trap("❌ " # name # ": FAIL. Mismatch null vs value.");
     };
   };
 };

82-111: Consider adding test cases for negative prices.

Given the implementations handle (or don't handle) negative prices differently, adding explicit test cases would clarify the expected behavior and catch regressions.

Add negative price test cases
   // 7. Power of 10
   assertEqual("Power of 10", funcToTest(100.0), ?(10000, -2));
+
+  // 8. Negative price - define expected behavior
+  // Option A: If negatives should be rejected:
+  assertEqual("Negative Price", funcToTest(-123.45), null);
+  // Option B: If negatives should work like positives:
+  // assertEqual("Negative Price", funcToTest(-123.45), ?(12345, -2));
 };

Comment thread test/float.test.mo Outdated
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/FloatKey.mo`:
- Around line 30-32: The public func sort<T>(self : [var T], key : (implicit : T
-> Float)) in FloatKey.mo currently has an empty body; implement it by
decomposing the Float key into a stable integer tuple (Nat32, Int) — mirroring
the approach used in Nat32Key.mo — and then call the existing underlying sort
primitive that accepts tuple keys (i.e., create a wrapper key function that maps
each T -> (Nat32, Int) and delegate to the same sort helper used by Nat32Key).
If you cannot implement decomposition now, replace the empty body with a clear
TODO or remove the function to avoid shipping a no-op API. Ensure you reference
the function name sort and the FloatKey.mo module when making the change.
- Around line 6-11: The Decomposer currently allows maxSignificantDigits up to
15 which can produce coefficients that overflow Nat32; change the contract to
cap maxSignificantDigits at 9 by updating the assert in the Decomposer (and any
related validation) from <= 15 to <= 9 (or alternatively switch coefficient
storage/creation to a larger integer type such as Nat64 if you prefer supporting
>9 digits); update the related symbol references (Decomposer,
maxSignificantDigits, digits) so all code that constructs or casts coefficients
uses the new limit (or the larger integer type) consistently to prevent
Nat32.fromNat traps at runtime.
- Around line 15-16: The guard in public func decompose(price : Float) should
explicitly reject infinities (positive and negative) because Float.isFinite does
not exist; update the initial check in decompose to return null for infinite
values (e.g., detect infinity via a comparison such as price != price + 1 or
explicit comparisons against Float.posInfinity/negInfinity) in addition to the
existing isNaN and <= 0 checks, and also replace the incorrect use of
Float.equal with Float.equalWithin for the epsilon-based comparison elsewhere in
this file so the three-argument comparison uses the correct stdlib function
(refer to decompose and the Float.equal call).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1cdb5986-fc95-475c-a277-06db338ecb26

📥 Commits

Reviewing files that changed from the base of the PR and between c891844 and 0c3b3f6.

📒 Files selected for processing (2)
  • src/FloatKey.mo
  • test/float.test.mo
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/float.test.mo

Comment thread src/FloatKey.mo
Comment on lines +6 to +11
public class Decomposer(maxSignificantDigits : Nat) {
let log10 = Float.log(10);
let eps = 1e-10;

let digits = Float.fromInt(maxSignificantDigits);
assert maxSignificantDigits <= 15;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Nat32 overflow risk when maxSignificantDigits > 9.

The assertion allows up to 15 significant digits, but Nat32 has a maximum value of ~4.3×10⁹. With 10+ significant digits, the coefficient can exceed Nat32 range (e.g., coefficient 9,999,999,999 for 10 digits), causing Nat32.fromNat to trap at runtime.

Either restrict maxSignificantDigits <= 9 or use a larger integer type for the coefficient.

🐛 Proposed fix to restrict digits
     let digits = Float.fromInt(maxSignificantDigits);
-    assert maxSignificantDigits <= 15;
+    assert maxSignificantDigits <= 9; // Nat32 max ≈ 4.3×10^9
📝 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
public class Decomposer(maxSignificantDigits : Nat) {
let log10 = Float.log(10);
let eps = 1e-10;
let digits = Float.fromInt(maxSignificantDigits);
assert maxSignificantDigits <= 15;
public class Decomposer(maxSignificantDigits : Nat) {
let log10 = Float.log(10);
let eps = 1e-10;
let digits = Float.fromInt(maxSignificantDigits);
assert maxSignificantDigits <= 9; // Nat32 max ≈ 4.3×10^9
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FloatKey.mo` around lines 6 - 11, The Decomposer currently allows
maxSignificantDigits up to 15 which can produce coefficients that overflow
Nat32; change the contract to cap maxSignificantDigits at 9 by updating the
assert in the Decomposer (and any related validation) from <= 15 to <= 9 (or
alternatively switch coefficient storage/creation to a larger integer type such
as Nat64 if you prefer supporting >9 digits); update the related symbol
references (Decomposer, maxSignificantDigits, digits) so all code that
constructs or casts coefficients uses the new limit (or the larger integer type)
consistently to prevent Nat32.fromNat traps at runtime.

Comment thread src/FloatKey.mo
Comment on lines +15 to +16
public func decompose(price : Float) : ?(Nat32, Int) {
if (Float.isNaN(price) or price <= 0) return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Float.isFinite is available in Motoko core library or similar infinity check
rg -n "isFinite|isInfinite" --type-add 'motoko:*.mo' --type motoko

Repository: research-ag/sort

Length of output: 42


🏁 Script executed:

cat -n src/FloatKey.mo | head -40

Repository: research-ag/sort

Length of output: 1389


🌐 Web query:

Motoko Float.isFinite standard library documentation

💡 Result:

No, there is no Float.isFinite function in the Motoko standard library base/Float module. The official documentation lists functions like isNaN, abs, sqrt, equalWithin, but no isFinite or isInfinite. To check if a Float is finite, use not Float.isNaN(x) and x != +inf and x != -inf, as Motoko supports infinity and NaN representations per IEEE 754.

Citations:


🏁 Script executed:

rg "Float\." src/FloatKey.mo | head -20

Repository: research-ag/sort

Length of output: 457


🌐 Web query:

Motoko Float.equal vs Float.equalWithin signature documentation

💡 Result:

In the Motoko base library Float module, Float.equal has the signature: func equal(x : Float, y : Float) : Bool It returns x == y and is marked @deprecated in favor of Float.equalWithin due to not accounting for numerical errors in floating-point comparisons. Float.equalWithin has the signature: func equalWithin(x : Float, y : Float, epsilon : Float) : Bool It determines if x is approximately equal to y within the tolerance epsilon (equivalent to Float.abs(x - y) <= epsilon for epsilon >= 0). It handles special cases like signed zeros and infinities being equal, but NaN comparisons return false. This is the recommended function for floating-point equality checks.

Citations:


Add explicit check for infinity; note that Float.isFinite does not exist in Motoko.

Positive infinity passes the current guard (isNaN is false, +∞ > 0 is true) and relies on NaN propagation through arithmetic to eventually return null. This is fragile and implementation-dependent. Motoko does not provide a Float.isFinite function, but you can check for infinities explicitly or use inequality checks (e.g., price != price + 1 to detect infinity). Alternatively, reject any value outside a valid range.

Note: Line 24 also contains a separate issue—Float.equal in Motoko's stdlib accepts only 2 arguments. For the 3-argument epsilon-based comparison, use Float.equalWithin instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FloatKey.mo` around lines 15 - 16, The guard in public func
decompose(price : Float) should explicitly reject infinities (positive and
negative) because Float.isFinite does not exist; update the initial check in
decompose to return null for infinite values (e.g., detect infinity via a
comparison such as price != price + 1 or explicit comparisons against
Float.posInfinity/negInfinity) in addition to the existing isNaN and <= 0
checks, and also replace the incorrect use of Float.equal with Float.equalWithin
for the epsilon-based comparison elsewhere in this file so the three-argument
comparison uses the correct stdlib function (refer to decompose and the
Float.equal call).

Comment thread src/FloatKey.mo
Comment on lines +30 to +32
public func sort<T>(self : [var T], key : (implicit : T -> Float)) {

};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Empty sort function body — incomplete implementation.

The function signature is defined but the body does nothing. Based on the patterns in src/Nat32Key.mo (snippets 2-3), this should decompose float keys to (Nat32, Int) tuples and delegate to the underlying sort primitives.

Either implement the sorting logic or remove/mark this as a TODO to avoid shipping dead code that silently fails to sort.

Would you like me to help draft an implementation that decomposes floats and delegates to the existing sort infrastructure?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FloatKey.mo` around lines 30 - 32, The public func sort<T>(self : [var
T], key : (implicit : T -> Float)) in FloatKey.mo currently has an empty body;
implement it by decomposing the Float key into a stable integer tuple (Nat32,
Int) — mirroring the approach used in Nat32Key.mo — and then call the existing
underlying sort primitive that accepts tuple keys (i.e., create a wrapper key
function that maps each T -> (Nat32, Int) and delegate to the same sort helper
used by Nat32Key). If you cannot implement decomposition now, replace the empty
body with a clear TODO or remove the function to avoid shipping a no-op API.
Ensure you reference the function name sort and the FloatKey.mo module when
making the change.

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