feat: execute source control flow and exact real arithmetic
This commit is contained in:
@@ -7,3 +7,8 @@ license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
num-bigint = "0.4"
|
||||
num-rational = "0.4"
|
||||
num-traits = "0.2"
|
||||
|
||||
@@ -258,6 +258,8 @@ pub(crate) fn read_only_opcode(opcode: Opcode) -> bool {
|
||||
| Opcode::PopJumpIfTrue
|
||||
| Opcode::ToBool
|
||||
| Opcode::UnaryNot
|
||||
| Opcode::BinaryArithmetic
|
||||
| Opcode::UnaryNegate
|
||||
| Opcode::CompareOp
|
||||
| Opcode::LoadDeref
|
||||
| Opcode::LoadClosure
|
||||
@@ -354,6 +356,7 @@ fn verify(
|
||||
| Opcode::LoadAttr
|
||||
| Opcode::StoreAttr => index < layout.names.len(),
|
||||
Opcode::CompareOp => compare_operation(argument).is_some(),
|
||||
Opcode::BinaryArithmetic => argument <= 3,
|
||||
Opcode::Copy => argument >= 1,
|
||||
Opcode::Swap => argument >= 2,
|
||||
Opcode::JumpForward
|
||||
@@ -394,8 +397,12 @@ fn verify(
|
||||
| Opcode::StoreDeref
|
||||
| Opcode::PopJumpIfFalse
|
||||
| Opcode::PopJumpIfTrue => (1, 1, 0),
|
||||
Opcode::ToBool | Opcode::UnaryNot | Opcode::LoadAttr | Opcode::Await => (1, 1, 1),
|
||||
Opcode::CompareOp => (2, 2, 1),
|
||||
Opcode::ToBool
|
||||
| Opcode::UnaryNot
|
||||
| Opcode::LoadAttr
|
||||
| Opcode::Await
|
||||
| Opcode::UnaryNegate => (1, 1, 1),
|
||||
Opcode::CompareOp | Opcode::BinaryArithmetic => (2, 2, 1),
|
||||
Opcode::StoreAttr => (2, 2, 0),
|
||||
Opcode::Copy => (argument, 0, 1),
|
||||
Opcode::Swap => (argument, 0, 0),
|
||||
|
||||
@@ -36,6 +36,8 @@ pub enum Opcode {
|
||||
MakeFunction = 28,
|
||||
BuildClass = 29,
|
||||
Await = 30,
|
||||
BinaryArithmetic = 31,
|
||||
UnaryNegate = 32,
|
||||
ExtendedArg = 255,
|
||||
}
|
||||
|
||||
@@ -73,6 +75,8 @@ impl Opcode {
|
||||
28 => Self::MakeFunction,
|
||||
29 => Self::BuildClass,
|
||||
30 => Self::Await,
|
||||
31 => Self::BinaryArithmetic,
|
||||
32 => Self::UnaryNegate,
|
||||
255 => Self::ExtendedArg,
|
||||
_ => return None,
|
||||
})
|
||||
@@ -88,6 +92,7 @@ impl Opcode {
|
||||
| Self::ToBool
|
||||
| Self::UnaryNot
|
||||
| Self::Await
|
||||
| Self::UnaryNegate
|
||||
| Self::BuildClass
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ pub use native::{
|
||||
NativeContext, NativeResult,
|
||||
};
|
||||
pub use number::{
|
||||
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
|
||||
ArithmeticOp, EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational,
|
||||
EndorReal,
|
||||
};
|
||||
pub use object::{
|
||||
AwaitPoll, CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Lossless number representations. Arithmetic and numeric equivalence are
|
||||
//! deliberately separate from representation equality.
|
||||
//! deliberately separate from representation equality; real operations preserve
|
||||
//! exact values unless a binary-float operand explicitly selects float mode.
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
@@ -39,7 +40,16 @@ impl EndorInteger {
|
||||
}
|
||||
|
||||
pub fn to_i64(&self) -> Option<i64> {
|
||||
self.to_string().parse().ok()
|
||||
let magnitude = self.digits.parse::<u64>().ok()?;
|
||||
if self.negative {
|
||||
if magnitude == (i64::MAX as u64) + 1 {
|
||||
Some(i64::MIN)
|
||||
} else {
|
||||
i64::try_from(magnitude).ok()?.checked_neg()
|
||||
}
|
||||
} else {
|
||||
i64::try_from(magnitude).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +309,12 @@ impl ObjectPayload for EndorNumberObject {
|
||||
Err(VmError::UnsupportedOperation("numeric hash"))
|
||||
}
|
||||
|
||||
fn compare(&self, other: &dyn ObjectPayload, _op: CompareOp) -> Result<Option<bool>, VmError> {
|
||||
if other.as_any().is::<Self>() {
|
||||
return Err(VmError::UnsupportedOperation("numeric comparison"));
|
||||
fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result<Option<bool>, VmError> {
|
||||
if let Some(other) = other.as_any().downcast_ref::<Self>() {
|
||||
if self.value.as_real().is_none() || other.value.as_real().is_none() {
|
||||
return Err(VmError::UnsupportedOperation("numeric comparison"));
|
||||
}
|
||||
return self.value.numeric_compare(&other.value, op).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -318,3 +331,222 @@ fn split_sign(text: &str) -> (bool, &str) {
|
||||
fn is_digits(text: &str) -> bool {
|
||||
!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// Source arithmetic is exact for integers/decimals/rationals. A binary-float
|
||||
/// operand explicitly selects IEEE floating-point arithmetic, as used by host
|
||||
/// geometry/math APIs. Complex arithmetic remains unsupported.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArithmeticOp {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
}
|
||||
impl ArithmeticOp {
|
||||
pub fn from_argument(value: u32) -> Option<Self> {
|
||||
match value {
|
||||
0 => Some(Self::Add),
|
||||
1 => Some(Self::Subtract),
|
||||
2 => Some(Self::Multiply),
|
||||
3 => Some(Self::Divide),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl EndorNumber {
|
||||
pub fn negated(&self) -> Result<Self, VmError> {
|
||||
if let Some(EndorReal::BinaryFloat(bits)) = self.as_real() {
|
||||
return Ok(Self::Real(EndorReal::BinaryFloat(bits ^ (1_u64 << 63))));
|
||||
}
|
||||
Self::from(0_i64).arithmetic(self, ArithmeticOp::Subtract)
|
||||
}
|
||||
/// Explicit narrowing for graphics/FFI boundaries; exact values themselves
|
||||
/// retain their representation. Non-real or non-finite values are rejected.
|
||||
pub fn to_f64(&self) -> Result<f64, VmError> {
|
||||
use num_traits::ToPrimitive;
|
||||
let real = self
|
||||
.as_real()
|
||||
.ok_or(VmError::UnsupportedOperation("complex to float"))?;
|
||||
let value = if let EndorReal::BinaryFloat(bits) = real {
|
||||
f64::from_bits(*bits)
|
||||
} else {
|
||||
exact_real(real)?.to_f64().ok_or(VmError::InvalidArgument(
|
||||
"number exceeds binary float range",
|
||||
))?
|
||||
};
|
||||
if !value.is_finite() {
|
||||
return Err(VmError::InvalidArgument("non-finite float conversion"));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
pub fn arithmetic(&self, other: &Self, op: ArithmeticOp) -> Result<Self, VmError> {
|
||||
if let (Some(a), Some(b)) = (self.as_i64(), other.as_i64()) {
|
||||
let integer = match op {
|
||||
ArithmeticOp::Add => a.checked_add(b),
|
||||
ArithmeticOp::Subtract => a.checked_sub(b),
|
||||
ArithmeticOp::Multiply => a.checked_mul(b),
|
||||
ArithmeticOp::Divide => a
|
||||
.checked_rem(b)
|
||||
.filter(|&r| r == 0)
|
||||
.and_then(|_| a.checked_div(b)),
|
||||
};
|
||||
if let Some(result) = integer {
|
||||
return Ok(Self::from(result));
|
||||
}
|
||||
}
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
let (a, b) = (
|
||||
self.as_real()
|
||||
.ok_or(VmError::UnsupportedOperation("complex arithmetic"))?,
|
||||
other
|
||||
.as_real()
|
||||
.ok_or(VmError::UnsupportedOperation("complex arithmetic"))?,
|
||||
);
|
||||
if matches!(a, EndorReal::BinaryFloat(_)) || matches!(b, EndorReal::BinaryFloat(_)) {
|
||||
let convert = |n: &EndorReal| -> Result<f64, VmError> {
|
||||
if let EndorReal::BinaryFloat(bits) = n {
|
||||
Ok(f64::from_bits(*bits))
|
||||
} else {
|
||||
exact_real(n)?.to_f64().ok_or(VmError::InvalidArgument(
|
||||
"number exceeds binary float range",
|
||||
))
|
||||
}
|
||||
};
|
||||
let (a, b) = (convert(a)?, convert(b)?);
|
||||
if matches!(op, ArithmeticOp::Divide) && b == 0. {
|
||||
return Err(VmError::InvalidArgument("division by zero"));
|
||||
}
|
||||
let result = match op {
|
||||
ArithmeticOp::Add => a + b,
|
||||
ArithmeticOp::Subtract => a - b,
|
||||
ArithmeticOp::Multiply => a * b,
|
||||
ArithmeticOp::Divide => a / b,
|
||||
};
|
||||
if !result.is_finite() {
|
||||
return Err(VmError::InvalidArgument("non-finite arithmetic result"));
|
||||
}
|
||||
return Ok(Self::binary_float(result));
|
||||
}
|
||||
let (a, b) = (exact_real(a)?, exact_real(b)?);
|
||||
if matches!(op, ArithmeticOp::Divide) && b.is_zero() {
|
||||
return Err(VmError::InvalidArgument("division by zero"));
|
||||
}
|
||||
let result = match op {
|
||||
ArithmeticOp::Add => a + b,
|
||||
ArithmeticOp::Subtract => a - b,
|
||||
ArithmeticOp::Multiply => a * b,
|
||||
ArithmeticOp::Divide => a / b,
|
||||
};
|
||||
if result.numer().bits() > 16_384 || result.denom().bits() > 16_384 {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"arithmetic result exceeds operation budget",
|
||||
));
|
||||
}
|
||||
if result.is_integer() {
|
||||
Self::integer(&result.to_integer().to_string())
|
||||
} else {
|
||||
Self::rational(&result.numer().to_string(), &result.denom().to_string())
|
||||
}
|
||||
}
|
||||
pub fn numeric_compare(&self, other: &Self, op: CompareOp) -> Result<bool, VmError> {
|
||||
use std::cmp::Ordering;
|
||||
let (a, b) = (
|
||||
self.as_real()
|
||||
.ok_or(VmError::UnsupportedOperation("complex comparison"))?,
|
||||
other
|
||||
.as_real()
|
||||
.ok_or(VmError::UnsupportedOperation("complex comparison"))?,
|
||||
);
|
||||
if let (Some(a), Some(b)) = (self.as_i64(), other.as_i64()) {
|
||||
return Ok(match op {
|
||||
CompareOp::Eq => a == b,
|
||||
CompareOp::Ne => a != b,
|
||||
CompareOp::Lt => a < b,
|
||||
CompareOp::Le => a <= b,
|
||||
CompareOp::Gt => a > b,
|
||||
CompareOp::Ge => a >= b,
|
||||
});
|
||||
}
|
||||
// Finite binary floats compare as exact binary rationals, so a large
|
||||
// integer never loses precision merely to answer an ordering question.
|
||||
let order = if let (EndorReal::BinaryFloat(a), EndorReal::BinaryFloat(b)) = (a, b) {
|
||||
f64::from_bits(*a).partial_cmp(&f64::from_bits(*b))
|
||||
} else {
|
||||
let nonfinite = |n: &EndorReal| match n {
|
||||
EndorReal::BinaryFloat(bits) if !f64::from_bits(*bits).is_finite() => {
|
||||
Some(f64::from_bits(*bits))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match (nonfinite(a), nonfinite(b)) {
|
||||
(Some(a), _) if a.is_nan() => None,
|
||||
(_, Some(b)) if b.is_nan() => None,
|
||||
(Some(a), _) => Some(if a.is_sign_positive() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}),
|
||||
(_, Some(b)) => Some(if b.is_sign_positive() {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}),
|
||||
_ => Some(exact_real(a)?.cmp(&exact_real(b)?)),
|
||||
}
|
||||
};
|
||||
Ok(match op {
|
||||
CompareOp::Eq => order == Some(Ordering::Equal),
|
||||
CompareOp::Ne => order != Some(Ordering::Equal),
|
||||
CompareOp::Lt => order == Some(Ordering::Less),
|
||||
CompareOp::Le => matches!(order, Some(Ordering::Less | Ordering::Equal)),
|
||||
CompareOp::Gt => order == Some(Ordering::Greater),
|
||||
CompareOp::Ge => matches!(order, Some(Ordering::Greater | Ordering::Equal)),
|
||||
})
|
||||
}
|
||||
}
|
||||
fn exact_real(value: &EndorReal) -> Result<num_rational::BigRational, VmError> {
|
||||
use num_bigint::BigInt;
|
||||
use num_rational::BigRational;
|
||||
let integer = |s: &str| -> Result<BigInt, VmError> {
|
||||
if s.len() > 4096 {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"numeric operand exceeds operation budget",
|
||||
));
|
||||
}
|
||||
s.parse()
|
||||
.map_err(|_| VmError::InvalidArgument("invalid exact number"))
|
||||
};
|
||||
match value {
|
||||
EndorReal::Integer(n) => Ok(BigRational::from_integer(integer(&n.to_string())?)),
|
||||
EndorReal::Rational(n) => Ok(BigRational::new(
|
||||
integer(&n.numerator.to_string())?,
|
||||
integer(&n.denominator.to_string())?,
|
||||
)),
|
||||
EndorReal::BinaryFloat(bits) => BigRational::from_float(f64::from_bits(*bits))
|
||||
.ok_or(VmError::InvalidArgument("non-finite exact conversion")),
|
||||
EndorReal::Decimal(n) => {
|
||||
let parts: Vec<_> = n.literal.split(['e', 'E']).collect();
|
||||
let exponent = if parts.len() == 2 {
|
||||
parts[1].parse::<i32>().map_err(|_| {
|
||||
VmError::InvalidArgument("numeric exponent exceeds operation budget")
|
||||
})?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let decimals = parts[0].split_once('.').map_or(0, |(_, s)| s.len()) as i64;
|
||||
let power = i64::from(exponent) - decimals;
|
||||
if power.abs() > 4096 {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"numeric exponent exceeds operation budget",
|
||||
));
|
||||
}
|
||||
let digits = integer(&parts[0].replace('.', ""))?;
|
||||
let factor = BigInt::from(10).pow(power.unsigned_abs() as u32);
|
||||
Ok(if power < 0 {
|
||||
BigRational::new(digits, factor)
|
||||
} else {
|
||||
BigRational::from_integer(digits * factor)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use objects::{ArithmeticOp, CompareOp, EndorNumber, EndorReal};
|
||||
fn equal(a: &EndorNumber, b: &EndorNumber) {
|
||||
assert!(a.numeric_compare(b, CompareOp::Eq).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn exact_real_arithmetic_keeps_large_values_and_cross_representation_equivalence() {
|
||||
let tenth = EndorNumber::decimal("0.10").unwrap();
|
||||
let fifth = EndorNumber::rational("2", "10").unwrap();
|
||||
equal(
|
||||
&tenth.arithmetic(&fifth, ArithmeticOp::Add).unwrap(),
|
||||
&EndorNumber::decimal("0.3").unwrap(),
|
||||
);
|
||||
let large = EndorNumber::integer("9223372036854775807").unwrap();
|
||||
equal(
|
||||
&large.arithmetic(&1_i64.into(), ArithmeticOp::Add).unwrap(),
|
||||
&EndorNumber::integer("9223372036854775808").unwrap(),
|
||||
);
|
||||
let min = EndorNumber::integer("-9223372036854775808").unwrap();
|
||||
assert_eq!(min.as_i64(), Some(i64::MIN));
|
||||
equal(
|
||||
&min.negated().unwrap(),
|
||||
&EndorNumber::integer("9223372036854775808").unwrap(),
|
||||
);
|
||||
equal(
|
||||
&EndorNumber::rational("2", "6").unwrap(),
|
||||
&EndorNumber::rational("1", "3").unwrap(),
|
||||
);
|
||||
assert!(
|
||||
EndorNumber::integer("9007199254740993")
|
||||
.unwrap()
|
||||
.numeric_compare(&EndorNumber::binary_float(9007199254740992.), CompareOp::Gt)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn ieee_signed_zero_nan_and_operation_limits_are_explicit() {
|
||||
let negative = EndorNumber::binary_float(0.).negated().unwrap();
|
||||
assert_eq!(
|
||||
negative.as_real(),
|
||||
Some(&EndorReal::BinaryFloat((-0.0_f64).to_bits()))
|
||||
);
|
||||
let nan = EndorNumber::binary_float(f64::NAN);
|
||||
assert!(!nan.numeric_compare(&nan, CompareOp::Eq).unwrap());
|
||||
assert!(nan.numeric_compare(&nan, CompareOp::Ne).unwrap());
|
||||
assert!(
|
||||
EndorNumber::from(1_i64)
|
||||
.arithmetic(&0_i64.into(), ArithmeticOp::Divide)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
EndorNumber::binary_float(f64::MAX)
|
||||
.arithmetic(&2_i64.into(), ArithmeticOp::Multiply)
|
||||
.is_err()
|
||||
);
|
||||
let huge = EndorNumber::decimal("1e99999999999999999").unwrap();
|
||||
assert_eq!(huge.to_string(), "1e99999999999999999");
|
||||
assert!(huge.arithmetic(&1_i64.into(), ArithmeticOp::Add).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user