Repository navigation
Standardize checked fixed-point math on Bunny #225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f372181
feat(numeric): adopt checked Bunny Q32.32 foundation
flyingrobots 829fe01
test: refresh provider fixture source binding for Bunny dependency
flyingrobots ec497c5
test: pin the public checked numeric profile identity
flyingrobots eff58d0
test: witness rounding before fixed-point overflow checks
flyingrobots 4fe28c5
Fix: align source build prerequisite with Bunny MSRV
flyingrobots File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| //! Checked fixed-point arithmetic for compiler consumers. | ||
| //! | ||
| //! Bunny 0.6.0 owns the arithmetic. This module selects its checked Q32.32 | ||
| //! subset without exposing saturating operators or floating-point conversions. | ||
| //! It does not add source syntax, a Core value tag, or Target instructions. | ||
|
|
||
| use bunny_num::FixedQ32_32; | ||
|
|
||
| /// Edict's checked integration profile, distinct from Bunny's SDL `q32.32` name. | ||
| pub const Q32_32_PROFILE: &str = "bunny.q32_32.checked/v1"; | ||
|
flyingrobots marked this conversation as resolved.
|
||
|
|
||
| /// Stable failures from checked fixed-point evaluation. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum NumericError { | ||
| /// The exact add/sub/neg result, or rounded mul/div result, exceeds i64. | ||
| Overflow, | ||
| /// Division has a zero raw divisor, including zero divided by zero. | ||
| DivisionByZero, | ||
| } | ||
|
|
||
| /// A signed Q32.32 value whose mathematical value is `raw / 2^32`. | ||
| /// | ||
| /// Every raw i64 is valid. Equality and ordering compare raw values exactly. | ||
| /// The Bunny representation stays private so callers cannot accidentally use | ||
| /// its saturating arithmetic operators through this checked API. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
| pub struct Q32_32(FixedQ32_32); | ||
|
|
||
| impl Q32_32 { | ||
| /// Preserves the supplied raw bits exactly, without integer scaling. | ||
| #[must_use] | ||
| pub const fn from_raw(raw: i64) -> Self { | ||
| Self(FixedQ32_32::from_raw(raw)) | ||
| } | ||
|
|
||
| /// Returns the exact signed raw representation, not a whole-number cast. | ||
| #[must_use] | ||
| pub const fn raw(self) -> i64 { | ||
| self.0.raw() | ||
| } | ||
|
|
||
| /// Adds two raw values exactly through Bunny's checked arithmetic. | ||
| /// | ||
| /// # Errors | ||
| /// Returns [`NumericError::Overflow`] when the exact sum cannot fit. | ||
| pub fn checked_add(self, rhs: Self) -> Result<Self, NumericError> { | ||
| self.0 | ||
| .checked_add(rhs.0) | ||
| .map(Self) | ||
| .ok_or(NumericError::Overflow) | ||
| } | ||
|
|
||
| /// Subtracts two raw values exactly through Bunny's checked arithmetic. | ||
| /// | ||
| /// # Errors | ||
| /// Returns [`NumericError::Overflow`] when the exact difference cannot fit. | ||
| pub fn checked_sub(self, rhs: Self) -> Result<Self, NumericError> { | ||
| self.0 | ||
| .checked_sub(rhs.0) | ||
| .map(Self) | ||
| .ok_or(NumericError::Overflow) | ||
| } | ||
|
|
||
| /// Negates a raw value exactly through Bunny's checked arithmetic. | ||
| /// | ||
| /// # Errors | ||
| /// Returns [`NumericError::Overflow`] for the minimum raw i64 value. | ||
| pub fn checked_neg(self) -> Result<Self, NumericError> { | ||
| self.0.checked_neg().map(Self).ok_or(NumericError::Overflow) | ||
| } | ||
|
|
||
| /// Multiplies with Bunny's wide intermediate and ties-to-even rounding. | ||
| /// | ||
| /// Quantization precedes the range check; a tiny nonzero product may round | ||
| /// to zero successfully. | ||
|
flyingrobots marked this conversation as resolved.
|
||
| /// | ||
| /// # Errors | ||
| /// Returns [`NumericError::Overflow`] when the rounded raw result cannot fit. | ||
| pub fn checked_mul(self, rhs: Self) -> Result<Self, NumericError> { | ||
| self.0 | ||
| .checked_mul(rhs.0) | ||
| .map(Self) | ||
| .ok_or(NumericError::Overflow) | ||
| } | ||
|
|
||
| /// Divides with Bunny's wide intermediate and ties-to-even rounding. | ||
| /// | ||
| /// Quantization precedes the range check; this is distinct from the | ||
| /// truncation-toward-zero rule for Edict's exact signed integer division. | ||
| /// | ||
| /// # Errors | ||
| /// Returns [`NumericError::DivisionByZero`] for a zero divisor, or | ||
| /// [`NumericError::Overflow`] when the rounded raw quotient cannot fit. | ||
| pub fn checked_div(self, rhs: Self) -> Result<Self, NumericError> { | ||
| if rhs.raw() == 0 { | ||
| return Err(NumericError::DivisionByZero); | ||
| } | ||
| self.0 | ||
| .checked_div(rhs.0) | ||
| .map(Self) | ||
| .ok_or(NumericError::Overflow) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| //! Literal raw vectors for the public checked numeric foundation. | ||
| use edict::numeric::{NumericError, Q32_32, Q32_32_PROFILE}; | ||
|
|
||
| fn raw(value: Result<Q32_32, NumericError>) -> Result<i64, NumericError> { | ||
| value.map(Q32_32::raw) | ||
| } | ||
|
|
||
| #[test] | ||
| fn public_numeric_profile_has_the_normative_identity() { | ||
| assert_eq!(Q32_32_PROFILE, "bunny.q32_32.checked/v1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn raw_values_preserve_bits_and_order() { | ||
| let values = [i64::MIN, -4_294_967_296, -1, 0, 1, 4_294_967_296, i64::MAX]; | ||
| for value in values { | ||
| assert_eq!(Q32_32::from_raw(value).raw(), value); | ||
| } | ||
| for pair in values.windows(2) { | ||
| assert!(Q32_32::from_raw(pair[0]) < Q32_32::from_raw(pair[1])); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn checked_linear_operations_preserve_exact_boundaries() { | ||
| for (left, right, expected) in [ | ||
| (0, 0, 0), | ||
| (4_294_967_296, -1, 4_294_967_295), | ||
| (i64::MAX - 1, 1, i64::MAX), | ||
| (i64::MIN + 1, -1, i64::MIN), | ||
| ] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_add(Q32_32::from_raw(right))), | ||
| Ok(expected) | ||
| ); | ||
| } | ||
| for (left, right, expected) in [ | ||
| (0, 0, 0), | ||
| (4_294_967_296, 1, 4_294_967_295), | ||
| (i64::MAX - 1, -1, i64::MAX), | ||
| (i64::MIN + 1, 1, i64::MIN), | ||
| ] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_sub(Q32_32::from_raw(right))), | ||
| Ok(expected) | ||
| ); | ||
| } | ||
| for (left, right) in [(i64::MAX, 1), (i64::MIN, -1)] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_add(Q32_32::from_raw(right))), | ||
| Err(NumericError::Overflow) | ||
| ); | ||
| } | ||
| for (left, right) in [(i64::MAX, -1), (i64::MIN, 1)] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_sub(Q32_32::from_raw(right))), | ||
| Err(NumericError::Overflow) | ||
| ); | ||
| } | ||
| for (value, expected) in [(0, 0), (1, -1), (-1, 1), (i64::MAX, -i64::MAX)] { | ||
| assert_eq!(raw(Q32_32::from_raw(value).checked_neg()), Ok(expected)); | ||
| } | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(i64::MIN).checked_neg()), | ||
| Err(NumericError::Overflow) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn multiplication_uses_signed_ties_to_even() { | ||
| for (left, right, expected) in [ | ||
| // Exact scaled product = i64::MAX + 8_365_928 / 2^32. | ||
| // The fractional remainder rounds down before the range check. | ||
| (199_032_858_228_936, 199_032_871_303_925, i64::MAX), | ||
| (1, 2_147_483_647, 0), | ||
| (1, 2_147_483_648, 0), | ||
| (1, 2_147_483_649, 1), | ||
| (3, 2_147_483_648, 2), | ||
| (5, 2_147_483_648, 2), | ||
| (-1, 2_147_483_647, 0), | ||
| (-1, 2_147_483_648, 0), | ||
| (-1, 2_147_483_649, -1), | ||
| (-3, 2_147_483_648, -2), | ||
| (-5, 2_147_483_648, -2), | ||
| (3, -2_147_483_648, -2), | ||
| (-3, -2_147_483_648, 2), | ||
| (i64::MAX, 4_294_967_296, i64::MAX), | ||
| (i64::MIN, 4_294_967_296, i64::MIN), | ||
| ] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_mul(Q32_32::from_raw(right))), | ||
| Ok(expected), | ||
| "{left} * {right}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn division_uses_signed_ties_to_even() { | ||
| for (left, right, expected) in [ | ||
| (1, 8_589_934_593, 0), | ||
| (1, 8_589_934_592, 0), | ||
| (1, 8_589_934_591, 1), | ||
| (3, 8_589_934_592, 2), | ||
| (5, 8_589_934_592, 2), | ||
| (-1, 8_589_934_593, 0), | ||
| (-1, 8_589_934_592, 0), | ||
| (-1, 8_589_934_591, -1), | ||
| (-3, 8_589_934_592, -2), | ||
| (-5, 8_589_934_592, -2), | ||
| (3, -8_589_934_592, -2), | ||
| (-3, -8_589_934_592, 2), | ||
| (i64::MAX, 4_294_967_296, i64::MAX), | ||
| (i64::MIN, 4_294_967_296, i64::MIN), | ||
| ] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_div(Q32_32::from_raw(right))), | ||
| Ok(expected), | ||
| "{left} / {right}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn checked_products_and_quotients_refuse_invalid_results() { | ||
| for (left, right) in [ | ||
| (i64::MAX, 8_589_934_592), | ||
| (i64::MIN, 8_589_934_592), | ||
| (i64::MIN, -4_294_967_296), | ||
| ] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_mul(Q32_32::from_raw(right))), | ||
| Err(NumericError::Overflow) | ||
| ); | ||
| } | ||
| for (left, right) in [(i64::MAX, 1), (i64::MIN, 1), (i64::MIN, -4_294_967_296)] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_div(Q32_32::from_raw(right))), | ||
| Err(NumericError::Overflow) | ||
| ); | ||
| } | ||
| for left in [i64::MIN, -1, 0, 1, i64::MAX] { | ||
| assert_eq!( | ||
| raw(Q32_32::from_raw(left).checked_div(Q32_32::from_raw(0))), | ||
| Err(NumericError::DivisionByZero) | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.