feat: execute source control flow and exact real arithmetic

This commit is contained in:
2026-11-12 10:48:27 +08:00
parent f3e1670c58
commit 5ff1c68f5f
28 changed files with 1045 additions and 117 deletions
+5
View File
@@ -7,3 +7,8 @@ license.workspace = true
[lints]
workspace = true
[dependencies]
num-bigint = "0.4"
num-rational = "0.4"
num-traits = "0.2"
+9 -2
View File
@@ -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),
+5
View File
@@ -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
)
}
+2 -1
View File
@@ -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,
+237 -5
View File
@@ -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)
})
}
}
}
+58
View File
@@ -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());
}