diff --git a/README.md b/README.md index 4f41d7e..bee7186 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Rust 编写的可嵌入运行时。当前实现 Endor 的 VM 基础设施:`End 尚无词法/语法前端、字节码执行器或渲染器,不承诺 Python 兼容。数据库只保存宿主元数据,不保存或恢复 VM 堆。对象基础层的逐段说明见 [EndorObject](docs/endor-object.md)。 +现有基础对象包括 `EndorFunctionObject`、`EndorNativeFunctionObject`、`EndorDictObject`、`EndorCellObject`、`EndorBaseExceptionObject`、`EndorStringObject` 和 `EndorNumberObject`。统一 `EndorNumber` 优先无损表示任意长度整数、精确十进制数、分数、IEEE 浮点和复数;不先实现重计算。语言函数及异常目前是数据基础,尚无语言函数执行和 raise/try/except。接口、示例与边界见 [基本对象和 Number](docs/endor-builtins.md)。 + ## 结构 | crate | 职责 | @@ -32,8 +34,8 @@ use aster_core::{Value, Vm}; fn main() -> Result<(), aster_core::VmError> { let mut vm = Vm::new()?; let object = vm.new_object()?; // 分配返回一个宿主持有的强引用。 - vm.set_attr(object, "x", Value::Integer(42))?; - assert_eq!(vm.get_attr(object, "x")?, Value::Integer(42)); + vm.set_attr(object, "x", Value::Number(42_i64.into()))?; + assert_eq!(vm.get_attr(object, "x")?, Value::Number(42_i64.into())); vm.retain(object)?; vm.release(object)?; assert_eq!(vm.ref_count(object)?, 1); @@ -165,7 +167,7 @@ docker stop aster-pg-test - 分配返回一个外部强引用;复制句柄或含句柄的 `Value` 不增加计数。额外持有使用 `retain`,结束持有使用 `release`。对象属性和载荷声明的边各自持有内部强引用;读属性与原生调用返回借用值,需长期持有时显式 retain。 - 最后一个强引用释放时,迭代清理依赖对象并调用 Rust Drop。`collect_cycles()` 从外部强引用追踪,显式回收不可达环;不会自动定时执行。销毁 VM 时统一释放剩余对象。 - 支持 repr/str、身份与内容比较、哈希、真值、属性读写删除、原生函数调用。操作由 Rust 载荷实现;不支持的操作返回错误。原生调用可返回标量或已有对象的借用句柄,当前不能在回调中修改 VM;没有语言函数执行器。 -- 标量与字符串继续直接保存在 `Value`,不要求全部值装箱。语言根类、继承、元类、描述符、语言异常及 VM 堆持久化均留待以后定义。 +- 标量与字符串可直接保存在 `Value`,数字统一为 `Value::Number`;需要对象身份时可分配 String/Number 对象。字典和 Cell 支持 VM 管理的可变引用;函数和异常保存数据并参与 GC。数值比较/哈希、语言函数执行、异常继承与传播、语言根类、元类、描述符及 VM 堆持久化留待以后实现。 下一阶段可围绕执行栈的强引用根管理和可变 VM 调用上下文推进执行器;GC 已有手动全堆版本,尚无分代、增量、并行收集或语言终结器/复活语义。详见 [架构](docs/architecture.md)、[对象基础层](docs/endor-object.md) 与 [开发交接](docs/development.md)。 diff --git a/crates/aster-core/src/builtins.rs b/crates/aster-core/src/builtins.rs new file mode 100644 index 0000000..879090e --- /dev/null +++ b/crates/aster-core/src/builtins.rs @@ -0,0 +1,393 @@ +use std::any::Any; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use crate::{CompareOp, ObjectPayload, ObjectRef, Value, VmError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndorStringObject { + text: String, +} + +impl EndorStringObject { + pub fn new(text: impl Into) -> Self { + Self { text: text.into() } + } + + pub fn as_str(&self) -> &str { + &self.text + } + + pub fn char_count(&self) -> usize { + self.text.chars().count() + } + + pub fn char_at(&self, index: usize) -> Option { + self.text.chars().nth(index) + } +} + +impl ObjectPayload for EndorStringObject { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "string" + } + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(format!("{:?}", self.text)) + } + fn str(&self, _identity: ObjectRef) -> Result { + Ok(self.text.clone()) + } + fn is_true(&self) -> Result { + Ok(!self.text.is_empty()) + } + fn hash(&self, _identity: ObjectRef) -> Result { + let mut hasher = DefaultHasher::new(); + self.text.hash(&mut hasher); + Ok(hasher.finish()) + } + fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result, VmError> { + let Some(other) = other.as_any().downcast_ref::() else { + return Ok(None); + }; + Ok(Some(match op { + CompareOp::Eq => self.text == other.text, + CompareOp::Ne => self.text != other.text, + CompareOp::Lt => self.text < other.text, + CompareOp::Le => self.text <= other.text, + CompareOp::Gt => self.text > other.text, + CompareOp::Ge => self.text >= other.text, + })) + } +} + +/// Insertion-ordered entries. Mutation is only exposed through Vm so every +/// key/value reference is validated, retained, traced, and released correctly. +/// Linear lookup is intentional for the initial representation layer. +#[derive(Debug, Clone, Default)] +pub struct EndorDictObject { + pub(crate) entries: Vec<(Value, Value)>, +} + +impl EndorDictObject { + pub fn new() -> Self { + Self::default() + } + pub fn len(&self) -> usize { + self.entries.len() + } + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + pub fn entries(&self) -> &[(Value, Value)] { + &self.entries + } +} + +impl ObjectPayload for EndorDictObject { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "dict" + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + for (key, value) in &self.entries { + trace_value(key, visit); + trace_value(value, visit); + } + } + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(format!("", self.len())) + } + fn is_true(&self) -> Result { + Ok(!self.is_empty()) + } + fn hash(&self, _identity: ObjectRef) -> Result { + Err(VmError::UnsupportedOperation("dict hash")) + } +} + +/// An empty cell differs from a cell containing Value::Nil. +#[derive(Debug, Clone, Default)] +pub struct EndorCellObject { + pub(crate) value: Option, +} + +impl EndorCellObject { + pub fn new(value: Option) -> Self { + Self { value } + } + pub fn value(&self) -> Option<&Value> { + self.value.as_ref() + } +} + +impl ObjectPayload for EndorCellObject { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "cell" + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + if let Some(value) = &self.value { + trace_value(value, visit); + } + } + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(if self.value.is_some() { + "" + } else { + "" + } + .into()) + } + fn hash(&self, _identity: ObjectRef) -> Result { + Err(VmError::UnsupportedOperation("cell hash")) + } +} + +/// Code data only: bytecode is opaque until an instruction format/executor +/// is defined. Constants may own managed objects through the enclosing function. +#[derive(Debug, Clone)] +pub struct EndorCode { + bytecode: Vec, + constants: Vec, + parameters: Vec, + free_variables: Vec, +} + +impl EndorCode { + pub fn new( + bytecode: Vec, + constants: Vec, + parameters: Vec, + free_variables: Vec, + ) -> Result { + for names in [¶meters, &free_variables] { + let mut seen = std::collections::BTreeSet::new(); + for name in names { + if name.is_empty() || !seen.insert(name) { + return Err(VmError::InvalidArgument( + "empty or duplicate code variable name", + )); + } + } + } + Ok(Self { + bytecode, + constants, + parameters, + free_variables, + }) + } + pub fn bytecode(&self) -> &[u8] { + &self.bytecode + } + pub fn constants(&self) -> &[Value] { + &self.constants + } + pub fn parameters(&self) -> &[String] { + &self.parameters + } + pub fn free_variables(&self) -> &[String] { + &self.free_variables + } +} + +#[derive(Debug, Clone)] +pub struct EndorFunctionObject { + name: String, + code: EndorCode, + globals: ObjectRef, + defaults: Vec, + closure: Vec, +} + +impl EndorFunctionObject { + pub fn new( + name: impl Into, + code: EndorCode, + globals: ObjectRef, + defaults: Vec, + closure: Vec, + ) -> Result { + if defaults.len() > code.parameters.len() || closure.len() != code.free_variables.len() { + return Err(VmError::InvalidArgument( + "function defaults or closure do not match code", + )); + } + Ok(Self { + name: name.into(), + code, + globals, + defaults, + closure, + }) + } + pub fn name(&self) -> &str { + &self.name + } + pub fn code(&self) -> &EndorCode { + &self.code + } + pub fn globals(&self) -> ObjectRef { + self.globals + } + pub fn defaults(&self) -> &[Value] { + &self.defaults + } + pub fn closure(&self) -> &[ObjectRef] { + &self.closure + } +} + +impl ObjectPayload for EndorFunctionObject { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "function" + } + fn allows_attributes(&self) -> bool { + true + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + visit(self.globals); + for reference in &self.closure { + visit(*reference); + } + for value in self.defaults.iter().chain(&self.code.constants) { + trace_value(value, visit); + } + } + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(format!("", self.name)) + } + fn call(&self, _arguments: &[Value]) -> Result { + Err(VmError::UnsupportedOperation("language function execution")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndorTracebackEntry { + pub function: String, + pub file: String, + pub line: usize, +} + +/// Common exception instance data, not an exception class or inheritance tree. +#[derive(Debug, Clone)] +pub struct EndorBaseExceptionObject { + name: String, + args: Vec, + cause: Option, + context: Option, + suppress_context: bool, + notes: Vec, + traceback: Vec, +} + +impl EndorBaseExceptionObject { + pub fn new(name: impl Into, args: Vec) -> Self { + Self { + name: name.into(), + args, + cause: None, + context: None, + suppress_context: false, + notes: Vec::new(), + traceback: Vec::new(), + } + } + pub fn with_cause(mut self, cause: ObjectRef) -> Self { + self.cause = Some(cause); + self.suppress_context = true; + self + } + pub fn with_context(mut self, context: ObjectRef) -> Self { + self.context = Some(context); + self + } + pub fn suppress_context(mut self, suppress: bool) -> Self { + self.suppress_context = suppress; + self + } + pub fn with_note(mut self, note: impl Into) -> Self { + self.notes.push(note.into()); + self + } + pub fn with_traceback(mut self, entry: EndorTracebackEntry) -> Self { + self.traceback.push(entry); + self + } + pub fn name(&self) -> &str { + &self.name + } + pub fn args(&self) -> &[Value] { + &self.args + } + pub fn cause(&self) -> Option { + self.cause + } + pub fn context(&self) -> Option { + self.context + } + pub fn context_suppressed(&self) -> bool { + self.suppress_context + } + pub fn notes(&self) -> &[String] { + &self.notes + } + pub fn traceback(&self) -> &[EndorTracebackEntry] { + &self.traceback + } +} + +impl ObjectPayload for EndorBaseExceptionObject { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "exception" + } + fn allows_attributes(&self) -> bool { + true + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + for value in &self.args { + trace_value(value, visit); + } + for reference in [self.cause, self.context].into_iter().flatten() { + visit(reference); + } + } + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(format!( + "{}({})", + self.name, + self.args + .iter() + .map(|arg| format!("{arg:?}")) + .collect::>() + .join(", ") + )) + } + fn str(&self, _identity: ObjectRef) -> Result { + Ok(match self.args.as_slice() { + [] => String::new(), + [Value::Text(text)] => text.clone(), + [Value::Number(number)] => number.to_string(), + args => format!("{args:?}"), + }) + } +} + +pub(crate) fn trace_value(value: &Value, visit: &mut dyn FnMut(ObjectRef)) { + if let Value::Object(reference) = value { + visit(*reference); + } +} diff --git a/crates/aster-core/src/heap.rs b/crates/aster-core/src/heap.rs index b04e736..f16270e 100644 --- a/crates/aster-core/src/heap.rs +++ b/crates/aster-core/src/heap.rs @@ -92,6 +92,27 @@ impl Heap { .ok_or(VmError::InvalidHandle) } + /// Transactional copy-on-update for built-in containers. The EndorObject + /// box and identity stay stable; only its payload and edge snapshot change. + /// All new edges are validated/retained before any old edge is released. + pub(crate) fn replace_payload( + &mut self, + reference: ObjectRef, + payload: impl ObjectPayload, + ) -> Result<(), VmError> { + self.get(reference)?; + let mut edges = Vec::new(); + payload.trace(&mut |target| edges.push(target)); + let increments = self.prepare_increments(&edges)?; + self.apply_increments(increments); + let object = self.get_mut(reference)?; + let old_edges = std::mem::replace(&mut object.payload_edges, edges); + let old_payload = std::mem::replace(&mut object.payload, Box::new(payload)); + self.release_edges(old_edges); + drop(old_payload); + Ok(()) + } + fn get_mut(&mut self, reference: ObjectRef) -> Result<&mut EndorObject, VmError> { self.get(reference)?; Ok(self.slots[reference.index].object.as_deref_mut().unwrap()) diff --git a/crates/aster-core/src/lib.rs b/crates/aster-core/src/lib.rs index ed503f1..da516ae 100644 --- a/crates/aster-core/src/lib.rs +++ b/crates/aster-core/src/lib.rs @@ -4,8 +4,8 @@ //! use aster_core::{Value, Vm}; //! let mut vm = Vm::new()?; //! let object = vm.new_object()?; // One owned external reference. -//! vm.set_attr(object, "x", Value::Integer(42))?; -//! assert_eq!(vm.get_attr(object, "x")?, Value::Integer(42)); +//! vm.set_attr(object, "x", Value::Number(42_i64.into()))?; +//! assert_eq!(vm.get_attr(object, "x")?, Value::Number(42_i64.into())); //! vm.retain(object)?; // Own an additional reference. //! vm.release(object)?; //! assert_eq!(vm.ref_count(object)?, 1); @@ -14,12 +14,23 @@ //! # Ok::<(), aster_core::VmError>(()) //! ``` +mod builtins; mod heap; +mod number; mod object; mod vm; +mod vm_builtins; +pub use builtins::{ + EndorBaseExceptionObject, EndorCellObject, EndorCode, EndorDictObject, EndorFunctionObject, + EndorStringObject, EndorTracebackEntry, +}; pub use heap::ObjectRef; +pub use number::{ + EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal, +}; pub use object::{ - CompareOp, EndorObject, NativeCall, NativeFunction, ObjectHeader, ObjectPayload, Record, Value, + CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall, NativeFunction, + ObjectHeader, ObjectPayload, Record, Value, }; pub use vm::{Vm, VmError}; diff --git a/crates/aster-core/src/number.rs b/crates/aster-core/src/number.rs new file mode 100644 index 0000000..9da9f6b --- /dev/null +++ b/crates/aster-core/src/number.rs @@ -0,0 +1,317 @@ +//! Lossless number representations. Arithmetic and numeric equivalence are +//! deliberately separate from representation equality. + +use std::any::Any; +use std::fmt; + +use crate::{CompareOp, ObjectPayload, ObjectRef, VmError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndorInteger { + negative: bool, + digits: String, +} + +impl EndorInteger { + /// Decimal digits of any length, limited only by available memory. + pub fn parse(text: &str) -> Result { + let (negative, digits) = split_sign(text); + if !is_digits(digits) { + return Err(VmError::InvalidArgument("invalid integer")); + } + let digits = digits.trim_start_matches('0'); + Ok(Self { + negative: negative && !digits.is_empty(), + digits: if digits.is_empty() { "0" } else { digits }.into(), + }) + } + + pub fn digits(&self) -> &str { + &self.digits + } + + pub fn is_negative(&self) -> bool { + self.negative + } + + pub fn is_zero(&self) -> bool { + self.digits == "0" + } + + pub fn to_i64(&self) -> Option { + self.to_string().parse().ok() + } +} + +impl fmt::Display for EndorInteger { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.negative { + f.write_str("-")?; + } + f.write_str(&self.digits) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndorDecimal { + literal: String, + zero: bool, +} + +impl EndorDecimal { + /// Retains all digits, trailing zeroes, sign, and an arbitrarily long + /// decimal exponent. No intermediate binary floating-point conversion. + pub fn parse(text: &str) -> Result { + let (_, unsigned) = split_sign(text); + let mut parts = unsigned.split(['e', 'E']); + let mantissa = parts.next().unwrap_or_default(); + if let Some(exponent) = parts.next() + && (!is_digits(split_sign(exponent).1) || parts.next().is_some()) + { + return Err(VmError::InvalidArgument("invalid decimal exponent")); + } + let mut decimal_point = false; + let mut digits = 0_usize; + let mut zero = true; + for byte in mantissa.bytes() { + match byte { + b'.' if !decimal_point => decimal_point = true, + b'0'..=b'9' => { + digits += 1; + zero &= byte == b'0'; + } + _ => return Err(VmError::InvalidArgument("invalid decimal")), + } + } + if digits == 0 { + return Err(VmError::InvalidArgument("decimal needs digits")); + } + Ok(Self { + literal: text.into(), + zero, + }) + } + + pub fn literal(&self) -> &str { + &self.literal + } + + pub fn is_zero(&self) -> bool { + self.zero + } +} + +impl fmt::Display for EndorDecimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.literal) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndorRational { + numerator: EndorInteger, + denominator: EndorInteger, +} + +impl EndorRational { + /// Normalizes denominator sign, without reducing or rounding the fraction. + pub fn new( + mut numerator: EndorInteger, + mut denominator: EndorInteger, + ) -> Result { + if denominator.is_zero() { + return Err(VmError::InvalidArgument("zero rational denominator")); + } + if denominator.negative { + denominator.negative = false; + if !numerator.is_zero() { + numerator.negative = !numerator.negative; + } + } + Ok(Self { + numerator, + denominator, + }) + } + + pub fn numerator(&self) -> &EndorInteger { + &self.numerator + } + + pub fn denominator(&self) -> &EndorInteger { + &self.denominator + } +} + +impl fmt::Display for EndorRational { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.numerator, self.denominator) + } +} + +/// Internal storage forms within one Endor number API, not language classes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EndorReal { + Integer(EndorInteger), + Decimal(EndorDecimal), + Rational(EndorRational), + /// IEEE-754 bits preserve signed zero, infinities and NaN payloads. + BinaryFloat(u64), +} + +impl EndorReal { + pub fn is_zero(&self) -> bool { + match self { + Self::Integer(value) => value.is_zero(), + Self::Decimal(value) => value.is_zero(), + Self::Rational(value) => value.numerator.is_zero(), + Self::BinaryFloat(bits) => f64::from_bits(*bits) == 0.0, + } + } +} + +impl fmt::Display for EndorReal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Integer(value) => value.fmt(f), + Self::Decimal(value) => value.fmt(f), + Self::Rational(value) => value.fmt(f), + Self::BinaryFloat(bits) => f64::from_bits(*bits).fmt(f), + } + } +} + +/// Unified, lossless number value. PartialEq/Eq compare representations, NOT +/// mathematical values: decimal scale and IEEE NaN bits remain observable. +/// Language-level comparison and hashing are not defined by these Rust traits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EndorNumber { + Real(EndorReal), + Complex { + real: EndorReal, + imaginary: EndorReal, + }, +} + +impl EndorNumber { + pub fn integer(text: &str) -> Result { + Ok(Self::Real(EndorReal::Integer(EndorInteger::parse(text)?))) + } + + pub fn decimal(text: &str) -> Result { + Ok(Self::Real(EndorReal::Decimal(EndorDecimal::parse(text)?))) + } + + pub fn rational(numerator: &str, denominator: &str) -> Result { + Ok(Self::Real(EndorReal::Rational(EndorRational::new( + EndorInteger::parse(numerator)?, + EndorInteger::parse(denominator)?, + )?))) + } + + pub fn binary_float(value: f64) -> Self { + Self::Real(EndorReal::BinaryFloat(value.to_bits())) + } + + pub fn complex(real: EndorReal, imaginary: EndorReal) -> Self { + Self::Complex { real, imaginary } + } + + pub fn as_real(&self) -> Option<&EndorReal> { + match self { + Self::Real(value) => Some(value), + Self::Complex { .. } => None, + } + } + + pub fn as_i64(&self) -> Option { + match self { + Self::Real(EndorReal::Integer(value)) => value.to_i64(), + _ => None, + } + } + + pub fn is_zero(&self) -> bool { + match self { + Self::Real(value) => value.is_zero(), + Self::Complex { real, imaginary } => real.is_zero() && imaginary.is_zero(), + } + } +} + +impl From for EndorNumber { + fn from(value: i64) -> Self { + Self::integer(&value.to_string()).expect("i64 always has valid decimal digits") + } +} + +impl From for EndorNumber { + fn from(value: f64) -> Self { + Self::binary_float(value) + } +} + +impl fmt::Display for EndorNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Real(value) => value.fmt(f), + Self::Complex { real, imaginary } => write!(f, "({real} + ({imaginary})i)"), + } + } +} + +#[derive(Debug, Clone)] +pub struct EndorNumberObject { + value: EndorNumber, +} + +impl EndorNumberObject { + pub fn new(value: EndorNumber) -> Self { + Self { value } + } + + pub fn value(&self) -> &EndorNumber { + &self.value + } +} + +impl ObjectPayload for EndorNumberObject { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "number" + } + + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(self.value.to_string()) + } + + fn is_true(&self) -> Result { + Ok(!self.value.is_zero()) + } + + fn hash(&self, _identity: ObjectRef) -> Result { + Err(VmError::UnsupportedOperation("numeric hash")) + } + + fn compare(&self, other: &dyn ObjectPayload, _op: CompareOp) -> Result, VmError> { + if other.as_any().is::() { + return Err(VmError::UnsupportedOperation("numeric comparison")); + } + Ok(None) + } +} + +fn split_sign(text: &str) -> (bool, &str) { + if let Some(rest) = text.strip_prefix('-') { + (true, rest) + } else { + (false, text.strip_prefix('+').unwrap_or(text)) + } +} + +fn is_digits(text: &str) -> bool { + !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()) +} diff --git a/crates/aster-core/src/object.rs b/crates/aster-core/src/object.rs index fb45a05..9187bbb 100644 --- a/crates/aster-core/src/object.rs +++ b/crates/aster-core/src/object.rs @@ -4,7 +4,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt; use std::hash::{Hash, Hasher}; -use crate::{ObjectRef, VmError}; +use crate::{EndorNumber, ObjectRef, VmError}; /// Immediate values stay outside the object heap. Object values are borrowed /// handles: copying or cloning one does not retain an external strong reference. @@ -12,8 +12,8 @@ use crate::{ObjectRef, VmError}; pub enum Value { Nil, Bool(bool), - Integer(i64), - Float(f64), + /// Rust equality preserves representation; it is not numeric equivalence. + Number(EndorNumber), Text(String), Object(ObjectRef), } @@ -45,7 +45,8 @@ impl CompareOp { /// /// Implementations must report every payload-owned object reference in trace(), /// including duplicates. Those edges must stay immutable after allocation; -/// mutable object references belong in VM-managed attributes instead. +/// mutable object references belong in VM-managed attributes or built-in +/// containers updated transactionally by the VM instead. /// Rust Drop releases host resources when the payload is destroyed. /// Equal payloads must provide compatible hashes, or reject hashing explicitly. pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { @@ -112,24 +113,32 @@ impl ObjectPayload for Record { } } -pub type NativeCall = fn(&[Value]) -> Result; +pub type EndorNativeCall = fn(&[Value]) -> Result; +pub type NativeCall = EndorNativeCall; #[derive(Debug)] -pub struct NativeFunction { +pub struct EndorNativeFunctionObject { name: String, function: NativeCall, } -impl NativeFunction { +impl EndorNativeFunctionObject { pub fn new(name: impl Into, function: NativeCall) -> Self { Self { name: name.into(), function, } } + + pub fn name(&self) -> &str { + &self.name + } } -impl ObjectPayload for NativeFunction { +/// Compatibility name for existing embedders. +pub type NativeFunction = EndorNativeFunctionObject; + +impl ObjectPayload for EndorNativeFunctionObject { fn as_any(&self) -> &dyn Any { self } @@ -190,7 +199,8 @@ impl EndorObject { } /// Visits owned edges, not external roots. Payload edges are snapshotted at - /// allocation; attributes are maintained by the VM's reference accounting. + /// allocation and refreshed on VM-managed payload replacement; attributes + /// are maintained by the VM's reference accounting. pub fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { for reference in &self.payload_edges { visit(*reference); diff --git a/crates/aster-core/src/vm.rs b/crates/aster-core/src/vm.rs index d819440..c1fe400 100644 --- a/crates/aster-core/src/vm.rs +++ b/crates/aster-core/src/vm.rs @@ -13,6 +13,8 @@ pub enum VmError { AttributeNotFound(String), UnsupportedOperation(&'static str), InvalidArgument(&'static str), + ObjectKindMismatch(&'static str), + EmptyCell, } impl fmt::Display for VmError { @@ -28,6 +30,8 @@ impl fmt::Display for VmError { write!(f, "object does not support {operation}") } Self::InvalidArgument(message) => f.write_str(message), + Self::ObjectKindMismatch(expected) => write!(f, "expected {expected} object"), + Self::EmptyCell => f.write_str("cell is empty"), } } } @@ -38,7 +42,7 @@ impl std::error::Error for VmError {} /// metaclasses or a Python type system. Dropping it releases all host resources. #[derive(Debug)] pub struct Vm { - heap: Heap, + pub(crate) heap: Heap, } impl Vm { @@ -53,6 +57,7 @@ impl Vm { /// Returns one external strong reference. Copies of the returned handle /// are borrowed; use retain/release to manage additional owned references. pub fn allocate(&mut self, payload: impl ObjectPayload) -> Result { + self.validate_builtin(&payload)?; self.heap.allocate(Box::new(payload)) } @@ -151,7 +156,7 @@ impl Vm { Ok(result) } - fn validate_value(&self, value: &Value) -> Result<(), VmError> { + pub(crate) fn validate_value(&self, value: &Value) -> Result<(), VmError> { if let Value::Object(reference) = value { self.heap.get(*reference)?; } diff --git a/crates/aster-core/src/vm_builtins.rs b/crates/aster-core/src/vm_builtins.rs new file mode 100644 index 0000000..83b4ee9 --- /dev/null +++ b/crates/aster-core/src/vm_builtins.rs @@ -0,0 +1,171 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use crate::{ + CompareOp, EndorBaseExceptionObject, EndorCellObject, EndorDictObject, EndorFunctionObject, + EndorNumber, EndorNumberObject, EndorStringObject, ObjectPayload, ObjectRef, Value, Vm, + VmError, +}; + +impl Vm { + pub(crate) fn validate_builtin(&self, payload: &dyn ObjectPayload) -> Result<(), VmError> { + if let Some(function) = payload.as_any().downcast_ref::() { + self.payload::(function.globals(), "dict")?; + for cell in function.closure() { + self.payload::(*cell, "cell")?; + } + } + if let Some(exception) = payload.as_any().downcast_ref::() { + for reference in [exception.cause(), exception.context()] + .into_iter() + .flatten() + { + self.payload::(reference, "exception")?; + } + } + Ok(()) + } + + pub fn payload( + &self, + reference: ObjectRef, + expected: &'static str, + ) -> Result<&T, VmError> { + self.object(reference)? + .payload() + .as_any() + .downcast_ref() + .ok_or(VmError::ObjectKindMismatch(expected)) + } + + pub fn new_string(&mut self, text: impl Into) -> Result { + self.allocate(EndorStringObject::new(text)) + } + + pub fn new_number(&mut self, number: EndorNumber) -> Result { + self.allocate(EndorNumberObject::new(number)) + } + + pub fn new_dict(&mut self) -> Result { + self.allocate(EndorDictObject::new()) + } + + /// Returns borrowed keys and values in insertion order. + pub fn dict_items(&self, dictionary: ObjectRef) -> Result<&[(Value, Value)], VmError> { + Ok(self + .payload::(dictionary, "dict")? + .entries()) + } + + pub fn dict_get(&self, dictionary: ObjectRef, key: &Value) -> Result, VmError> { + let dict = self.payload::(dictionary, "dict")?; + self.key_hash(key)?; + Ok(self + .dict_position(dict, key)? + .map(|index| dict.entries[index].1.clone())) + } + + pub fn dict_set( + &mut self, + dictionary: ObjectRef, + key: Value, + value: Value, + ) -> Result<(), VmError> { + let mut dict = self.payload::(dictionary, "dict")?.clone(); + self.key_hash(&key)?; + self.validate_value(&value)?; + if let Some(index) = self.dict_position(&dict, &key)? { + // Preserve the first key object and insertion position. + dict.entries[index].1 = value; + } else { + dict.entries.push((key, value)); + } + self.heap.replace_payload(dictionary, dict) + } + + pub fn dict_delete(&mut self, dictionary: ObjectRef, key: &Value) -> Result { + let mut dict = self.payload::(dictionary, "dict")?.clone(); + self.key_hash(key)?; + let Some(index) = self.dict_position(&dict, key)? else { + return Ok(false); + }; + dict.entries.remove(index); + self.heap.replace_payload(dictionary, dict)?; + Ok(true) + } + + pub fn dict_clear(&mut self, dictionary: ObjectRef) -> Result<(), VmError> { + self.payload::(dictionary, "dict")?; + self.heap + .replace_payload(dictionary, EndorDictObject::new()) + } + + fn dict_position(&self, dict: &EndorDictObject, key: &Value) -> Result, VmError> { + for (index, (existing, _)) in dict.entries.iter().enumerate() { + if self.key_equal(existing, key)? { + return Ok(Some(index)); + } + } + Ok(None) + } + + fn key_hash(&self, value: &Value) -> Result { + self.validate_value(value)?; + let mut hasher = DefaultHasher::new(); + match value { + Value::Nil => "nil".hash(&mut hasher), + Value::Bool(value) => value.hash(&mut hasher), + Value::Text(value) => value.hash(&mut hasher), + Value::Number(_) => return Err(VmError::UnsupportedOperation("numeric hash")), + Value::Object(reference) => return self.hash(*reference), + } + Ok(hasher.finish()) + } + + fn key_equal(&self, left: &Value, right: &Value) -> Result { + let text = |value: &Value| -> Result, VmError> { + Ok(match value { + Value::Text(text) => Some(text.clone()), + Value::Object(reference) => self + .object(*reference)? + .payload() + .as_any() + .downcast_ref::() + .map(|string| string.as_str().to_owned()), + _ => None, + }) + }; + if let (Some(left), Some(right)) = (text(left)?, text(right)?) { + return Ok(left == right); + } + match (left, right) { + (Value::Object(left), Value::Object(right)) => { + self.compare(*left, *right, CompareOp::Eq) + } + _ => Ok(left == right), + } + } + + pub fn new_cell(&mut self, value: Option) -> Result { + self.allocate(EndorCellObject::new(value)) + } + + /// Reads a borrowed value; an empty cell is not Value::Nil. + pub fn cell_get(&self, cell: ObjectRef) -> Result { + self.payload::(cell, "cell")? + .value() + .cloned() + .ok_or(VmError::EmptyCell) + } + + pub fn cell_set(&mut self, cell: ObjectRef, value: Value) -> Result<(), VmError> { + self.payload::(cell, "cell")?; + self.heap + .replace_payload(cell, EndorCellObject::new(Some(value))) + } + + pub fn cell_clear(&mut self, cell: ObjectRef) -> Result<(), VmError> { + self.payload::(cell, "cell")?; + self.heap.replace_payload(cell, EndorCellObject::default()) + } +} diff --git a/crates/aster-core/tests/builtins.rs b/crates/aster-core/tests/builtins.rs new file mode 100644 index 0000000..99d0b22 --- /dev/null +++ b/crates/aster-core/tests/builtins.rs @@ -0,0 +1,343 @@ +use aster_core::{ + CompareOp, EndorBaseExceptionObject, EndorCode, EndorDictObject, EndorFunctionObject, + EndorInteger, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorReal, + EndorStringObject, EndorTracebackEntry, Value, Vm, VmError, +}; + +fn text(value: &str) -> Value { + Value::Text(value.into()) +} + +#[test] +fn numbers_preserve_large_digits_decimal_scale_exponents_and_fraction_parts() { + let huge = "123456789012345678901234567890123456789012345678901234567890"; + assert_eq!(EndorNumber::integer(huge).unwrap().to_string(), huge); + assert_eq!(EndorNumber::integer("-000").unwrap().to_string(), "0"); + assert_eq!(EndorNumber::integer("+0012").unwrap().as_i64(), Some(12)); + assert_eq!(EndorNumber::integer(huge).unwrap().as_i64(), None); + for literal in [ + "-0.0000", + "1.2300000000000000000001", + ".5", + "1.", + "2e+999999999999999999999999999999", + ] { + assert_eq!(EndorNumber::decimal(literal).unwrap().to_string(), literal); + } + assert_ne!( + EndorNumber::decimal("1.00").unwrap(), + EndorNumber::decimal("1.0").unwrap() + ); + assert_eq!( + EndorNumber::rational("2", "-6").unwrap().to_string(), + "-2/6" + ); + assert_eq!(EndorNumber::rational("0", "-6").unwrap().to_string(), "0/6"); + assert!(EndorNumber::rational("1", "0").is_err()); + for invalid in ["", "+", " 1", "1 ", "1_000", "12", "1.0"] { + assert!(EndorNumber::integer(invalid).is_err(), "{invalid}"); + } + for invalid in ["", ".", "1.2.3", "1e", "1e+", "1e2e3", "nan", " 0.1", "--1"] { + assert!(EndorNumber::decimal(invalid).is_err(), "{invalid}"); + } +} + +#[test] +fn number_objects_preserve_ieee_bits_and_reject_undefined_numeric_operations() { + let mut vm = Vm::new().unwrap(); + let bits = 0x7ff8_0000_0000_0042; + let nan = EndorNumber::binary_float(f64::from_bits(bits)); + assert_eq!(nan.as_real(), Some(&EndorReal::BinaryFloat(bits))); + assert_ne!( + EndorNumber::binary_float(-0.0), + EndorNumber::binary_float(0.0) + ); + assert!(!EndorNumber::binary_float(f64::INFINITY).is_zero()); + let zero = vm + .new_number(EndorNumber::decimal("-0.000e99999999999").unwrap()) + .unwrap(); + assert!(!vm.is_true(zero).unwrap()); + let complex = EndorNumber::complex( + EndorReal::Integer(EndorInteger::parse("3").unwrap()), + EndorNumber::rational("1", "3") + .unwrap() + .as_real() + .unwrap() + .clone(), + ); + let object = vm.new_number(complex).unwrap(); + assert_eq!(vm.str(object).unwrap(), "(3 + (1/3)i)"); + assert_eq!( + vm.hash(object), + Err(VmError::UnsupportedOperation("numeric hash")) + ); + assert_eq!( + vm.compare(object, zero, CompareOp::Eq), + Err(VmError::UnsupportedOperation("numeric comparison")) + ); + assert!( + vm.payload::(object, "number") + .unwrap() + .value() + .as_real() + .is_none() + ); +} + +#[test] +fn strings_use_unicode_characters_and_content_equality() { + let mut vm = Vm::new().unwrap(); + let a = vm.new_string("Endor中文🦀").unwrap(); + let b = vm.new_string("Endor中文🦀").unwrap(); + let empty = vm.new_string("").unwrap(); + let string = vm.payload::(a, "string").unwrap(); + assert_eq!(string.char_count(), 8); + assert_eq!(string.char_at(7), Some('🦀')); + assert_eq!(string.char_at(8), None); + assert!(vm.compare(a, b, CompareOp::Eq).unwrap()); + assert_eq!(vm.hash(a).unwrap(), vm.hash(b).unwrap()); + assert_eq!(vm.str(a).unwrap(), "Endor中文🦀"); + assert!(!vm.is_true(empty).unwrap()); +} + +#[test] +fn dictionaries_keep_order_and_first_key_and_own_keys_and_values() { + let mut vm = Vm::new().unwrap(); + let dict = vm.new_dict().unwrap(); + let key = vm.new_string("answer").unwrap(); + let old = vm.new_object().unwrap(); + vm.dict_set(dict, Value::Object(key), Value::Object(old)) + .unwrap(); + vm.release(key).unwrap(); + vm.release(old).unwrap(); + vm.dict_set(dict, text("second"), Value::Nil).unwrap(); + vm.dict_set(dict, text("answer"), text("replacement")) + .unwrap(); + assert_eq!( + vm.dict_get(dict, &text("answer")).unwrap(), + Some(text("replacement")) + ); + assert_eq!(vm.dict_items(dict).unwrap()[0].0, Value::Object(key)); + assert_eq!(vm.dict_items(dict).unwrap().len(), 2); + assert!(matches!(vm.object(old), Err(VmError::InvalidHandle))); + assert!(vm.dict_delete(dict, &text("answer")).unwrap()); + assert!(matches!(vm.object(key), Err(VmError::InvalidHandle))); + assert!(!vm.dict_delete(dict, &text("missing")).unwrap()); + vm.dict_clear(dict).unwrap(); + assert!(!vm.is_true(dict).unwrap()); +} + +#[test] +fn failed_container_writes_leave_values_and_counts_unchanged() { + let mut vm = Vm::new().unwrap(); + let mut other = Vm::new().unwrap(); + let foreign = other.new_object().unwrap(); + let dict = vm.new_dict().unwrap(); + let child = vm.new_object().unwrap(); + vm.dict_set(dict, text("x"), Value::Object(child)).unwrap(); + assert_eq!( + vm.dict_set(dict, text("x"), Value::Object(foreign)), + Err(VmError::ForeignHandle) + ); + assert_eq!( + vm.dict_get(dict, &text("x")).unwrap(), + Some(Value::Object(child)) + ); + assert_eq!(vm.ref_count(child).unwrap(), 2); + assert_eq!( + vm.dict_set(dict, Value::Number(1_i64.into()), Value::Nil), + Err(VmError::UnsupportedOperation("numeric hash")) + ); + assert_eq!( + vm.dict_set(dict, Value::Object(dict), Value::Nil), + Err(VmError::UnsupportedOperation("dict hash")) + ); + let cell = vm.new_cell(Some(Value::Object(child))).unwrap(); + assert_eq!( + vm.cell_set(cell, Value::Object(foreign)), + Err(VmError::ForeignHandle) + ); + assert_eq!(vm.cell_get(cell).unwrap(), Value::Object(child)); + let dead = vm.new_object().unwrap(); + vm.release(dead).unwrap(); + assert_eq!( + vm.cell_set(cell, Value::Object(dead)), + Err(VmError::InvalidHandle) + ); + assert_eq!(vm.ref_count(child).unwrap(), 3); + assert_eq!( + vm.dict_clear(child), + Err(VmError::ObjectKindMismatch("dict")) + ); +} + +#[test] +fn cells_share_rebinding_distinguish_empty_and_none_and_keep_stable_objects() { + let mut vm = Vm::new().unwrap(); + let cell = vm.new_cell(None).unwrap(); + assert_eq!(vm.cell_get(cell), Err(VmError::EmptyCell)); + vm.cell_set(cell, Value::Nil).unwrap(); + assert_eq!(vm.cell_get(cell).unwrap(), Value::Nil); + let child = vm.new_object().unwrap(); + vm.cell_set(cell, Value::Object(child)).unwrap(); + vm.release(child).unwrap(); + vm.cell_set(cell, Value::Object(child)).unwrap(); + assert_eq!(vm.ref_count(child).unwrap(), 1); + vm.cell_clear(cell).unwrap(); + assert!(matches!(vm.object(child), Err(VmError::InvalidHandle))); + assert_eq!(vm.cell_get(cell), Err(VmError::EmptyCell)); +} + +#[test] +fn container_cycles_and_duplicate_edges_are_collected() { + let mut vm = Vm::new().unwrap(); + let dict = vm.new_dict().unwrap(); + let cell = vm.new_cell(Some(Value::Object(dict))).unwrap(); + vm.dict_set(dict, text("a"), Value::Object(cell)).unwrap(); + vm.dict_set(dict, text("b"), Value::Object(cell)).unwrap(); + assert_eq!(vm.ref_count(cell).unwrap(), 3); + vm.release(dict).unwrap(); + vm.release(cell).unwrap(); + assert_eq!(vm.collect_cycles(), 2); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn functions_retain_globals_constants_defaults_and_shared_closure_cells() { + let mut vm = Vm::new().unwrap(); + let globals = vm.new_dict().unwrap(); + let constant = vm.new_string("constant").unwrap(); + let cell = vm.new_cell(Some(text("first"))).unwrap(); + let code = EndorCode::new( + vec![0], + vec![Value::Object(constant)], + vec!["argument".into()], + vec!["captured".into()], + ) + .unwrap(); + let function = vm + .allocate( + EndorFunctionObject::new( + "read", + code.clone(), + globals, + vec![Value::Object(constant)], + vec![cell], + ) + .unwrap(), + ) + .unwrap(); + let second = vm + .allocate( + EndorFunctionObject::new("read_again", code, globals, vec![], vec![cell]).unwrap(), + ) + .unwrap(); + assert_eq!(vm.ref_count(constant).unwrap(), 4); + vm.release(constant).unwrap(); + vm.release(cell).unwrap(); + vm.cell_set(cell, text("updated")).unwrap(); + for reference in [function, second] { + let f = vm + .payload::(reference, "function") + .unwrap(); + assert_eq!(vm.cell_get(f.closure()[0]).unwrap(), text("updated")); + } + assert_eq!( + vm.call(function, &[]), + Err(VmError::UnsupportedOperation("language function execution")) + ); + vm.dict_set(globals, text("read"), Value::Object(function)) + .unwrap(); + vm.release(globals).unwrap(); + vm.release(function).unwrap(); + vm.release(second).unwrap(); + assert_eq!(vm.collect_cycles(), 4); +} + +#[test] +fn malformed_function_environments_are_rejected_without_retaining_any_edges() { + let mut vm = Vm::new().unwrap(); + let globals = vm.new_object().unwrap(); + let code = EndorCode::new(vec![], vec![], vec![], vec![]).unwrap(); + let function = EndorFunctionObject::new("bad", code, globals, vec![], vec![]).unwrap(); + assert_eq!( + vm.allocate(function), + Err(VmError::ObjectKindMismatch("dict")) + ); + assert_eq!(vm.ref_count(globals).unwrap(), 1); + assert!(EndorCode::new(vec![], vec![], vec!["x".into(), "x".into()], vec![]).is_err()); + let code = EndorCode::new(vec![], vec![], vec![], vec!["x".into()]).unwrap(); + assert!(EndorFunctionObject::new("bad", code, globals, vec![], vec![]).is_err()); +} + +#[test] +fn exceptions_preserve_arguments_chains_notes_and_traceback_without_type_classes() { + let mut vm = Vm::new().unwrap(); + let argument = vm.new_string("detail").unwrap(); + let cause = vm + .allocate(EndorBaseExceptionObject::new( + "ValueError", + vec![Value::Object(argument)], + )) + .unwrap(); + let exception = vm + .allocate( + EndorBaseExceptionObject::new("RuntimeError", vec![text("failed")]) + .with_cause(cause) + .with_context(cause) + .with_note("while loading") + .with_traceback(EndorTracebackEntry { + function: "load".into(), + file: "demo.endor".into(), + line: 12, + }), + ) + .unwrap(); + assert_eq!(vm.ref_count(cause).unwrap(), 3); + vm.release(argument).unwrap(); + vm.release(cause).unwrap(); + let data = vm + .payload::(exception, "exception") + .unwrap(); + assert!(data.context_suppressed()); + assert_eq!(data.notes(), &["while loading"]); + assert_eq!(data.traceback()[0].line, 12); + assert_eq!(vm.str(exception).unwrap(), "failed"); + vm.release(exception).unwrap(); + assert_eq!(vm.object_count(), 0); + let wrong = vm.new_dict().unwrap(); + assert_eq!( + vm.allocate(EndorBaseExceptionObject::new("Error", vec![]).with_cause(wrong)), + Err(VmError::ObjectKindMismatch("exception")) + ); +} + +fn echo(arguments: &[Value]) -> Result { + match arguments { + [value] => Ok(value.clone()), + _ => Err(VmError::InvalidArgument("expected one value")), + } +} + +#[test] +fn native_functions_pass_lossless_numbers_and_borrowed_objects() { + let mut vm = Vm::new().unwrap(); + let function = vm + .allocate(EndorNativeFunctionObject::new("echo", echo)) + .unwrap(); + let number = Value::Number(EndorNumber::decimal("12345678901234567890.000100").unwrap()); + assert_eq!( + vm.call(function, std::slice::from_ref(&number)).unwrap(), + number + ); + let dict = vm.new_dict().unwrap(); + assert_eq!( + vm.call(function, &[Value::Object(dict)]).unwrap(), + Value::Object(dict) + ); + assert_eq!(vm.ref_count(dict).unwrap(), 1); + assert_eq!( + vm.payload::(dict, "dict").unwrap().len(), + 0 + ); +} diff --git a/crates/aster-core/tests/runtime.rs b/crates/aster-core/tests/runtime.rs index 12a4e35..01452b6 100644 --- a/crates/aster-core/tests/runtime.rs +++ b/crates/aster-core/tests/runtime.rs @@ -164,9 +164,13 @@ fn replacing_the_same_edge_does_not_destroy_the_target() { vm.release(target).unwrap(); vm.set_attr(owner, "child", Value::Object(target)).unwrap(); assert_eq!(vm.ref_count(target).unwrap(), 1); - vm.set_attr(owner, "child", Value::Integer(7)).unwrap(); + vm.set_attr(owner, "child", Value::Number(7_i64.into())) + .unwrap(); assert!(matches!(vm.object(target), Err(VmError::InvalidHandle))); - assert_eq!(vm.get_attr(owner, "child").unwrap(), Value::Integer(7)); + assert_eq!( + vm.get_attr(owner, "child").unwrap(), + Value::Number(7_i64.into()) + ); } #[test] @@ -194,7 +198,8 @@ fn failed_foreign_and_stale_writes_preserve_the_old_attribute() { let foreign = other.new_object().unwrap(); let stale = vm.new_object().unwrap(); vm.release(stale).unwrap(); - vm.set_attr(owner, "value", Value::Integer(1)).unwrap(); + vm.set_attr(owner, "value", Value::Number(1_i64.into())) + .unwrap(); assert_eq!( vm.set_attr(owner, "value", Value::Object(foreign)), Err(VmError::ForeignHandle) @@ -203,7 +208,10 @@ fn failed_foreign_and_stale_writes_preserve_the_old_attribute() { vm.set_attr(owner, "value", Value::Object(stale)), Err(VmError::InvalidHandle) ); - assert_eq!(vm.get_attr(owner, "value").unwrap(), Value::Integer(1)); + assert_eq!( + vm.get_attr(owner, "value").unwrap(), + Value::Number(1_i64.into()) + ); assert_eq!(vm.retain(foreign), Err(VmError::ForeignHandle)); assert_eq!(vm.release(foreign), Err(VmError::ForeignHandle)); assert_eq!(vm.is_same(owner, foreign), Err(VmError::ForeignHandle)); @@ -378,10 +386,15 @@ fn reflected_operations_and_explicitly_unsupported_hashing_are_respected() { fn add(arguments: &[Value]) -> Result { match arguments { - [Value::Integer(left), Value::Integer(right)] => left - .checked_add(*right) - .map(Value::Integer) - .ok_or(VmError::InvalidArgument("integer overflow")), + [Value::Number(left), Value::Number(right)] => { + let (left, right) = left + .as_i64() + .zip(right.as_i64()) + .ok_or(VmError::InvalidArgument("expected two integers"))?; + left.checked_add(right) + .map(|value| Value::Number(value.into())) + .ok_or(VmError::InvalidArgument("integer overflow")) + } _ => Err(VmError::InvalidArgument("expected two integers")), } } @@ -392,16 +405,22 @@ fn native_calls_dispatch_validate_arguments_and_propagate_errors() { let function = vm.allocate(NativeFunction::new("add", add)).unwrap(); assert_eq!(vm.repr(function).unwrap(), ""); assert_eq!( - vm.call(function, &[Value::Integer(2), Value::Integer(3)]) - .unwrap(), - Value::Integer(5) + vm.call( + function, + &[Value::Number(2_i64.into()), Value::Number(3_i64.into())] + ) + .unwrap(), + Value::Number(5_i64.into()) ); assert_eq!( vm.call(function, &[]), Err(VmError::InvalidArgument("expected two integers")) ); assert_eq!( - vm.call(function, &[Value::Integer(i64::MAX), Value::Integer(1)]), + vm.call( + function, + &[Value::Number(i64::MAX.into()), Value::Number(1_i64.into())] + ), Err(VmError::InvalidArgument("integer overflow")) ); assert_eq!( diff --git a/docs/architecture.md b/docs/architecture.md index e12f1dc..bc70beb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,7 @@ allocate/new_object 返回一个外部强引用。ObjectRef 是可复制的借 属性写入先验证目标、被引用对象和计数溢出,再增加新引用、替换属性、释放旧引用;同目标替换不会短暂释放对象。属性读取和原生调用返回借用值,宿主需要跨越所有者生命周期使用时应先 retain。没有外部引用时,release 返回 NoOwnedReference,不能误释放内部属性引用。release 不代表删除任意对象。 -载荷通过 trace 报告自己持有的引用;分配时验证全部边并记录快照,相同引用出现两次就计数两次。分配失败不改变已存在对象的计数。载荷持有的边在分配后必须保持不变,可变引用通过 VM 托管属性修改。扩展未声明的借用句柄不受 GC 保护,访问或返回时仍需通过 VM 验证。 +载荷通过 trace 报告自己持有的引用;分配时验证全部边并记录快照,相同引用出现两次就计数两次。分配失败不改变已存在对象的计数。自定义载荷不能自行修改分配后的引用;内置字典和 Cell 通过 VM 事务性替换载荷与快照更新引用,先保留新边,再释放旧边。对象本身的 Box 和身份不变。扩展未声明的借用句柄不受 GC 保护,访问或返回时仍需通过 VM 验证。 ### 清理与循环回收 @@ -32,7 +32,9 @@ ObjectPayload 定义 repr、str、is_true、hash、compare、call 及属性许 Record 支持托管属性,NativeFunction 包装 Rust 函数指针。原生调用先验证参数句柄,再执行回调,再验证结果句柄。回调只计算返回值,不能修改或分配当前 VM;对象结果是借用句柄。自定义载荷可覆盖操作,若提供内容相等语义,必须保持哈希一致,或显式拒绝哈希。错误通过 Result 返回,不是语言异常机制。 -Value 中标量和字符串不强制堆分配。Float 暂用 Rust f64 语义;完整字符串、数值对象和语言类型规则尚未定义。更详细的代码说明见 endor-object.md。 +Value 中标量和字符串不强制堆分配;数字统一为 EndorNumber,同时可装箱为 EndorNumberObject。整数和精确小数通过文本保留任意长度数字,分数保留分子分母,浮点保留 IEEE 位,复数保存两个实数表示。当前不实现数值运算、跨表示数值相等或哈希,数字暂不能当字典键。Rust 值相等只比较表示,不是语言数字相等。 + +EndorDictObject 保留插入顺序,当前线性查询;EndorCellObject 提供共享可变槽。EndorFunctionObject 保存代码数据、全局字典、默认参数和闭包 Cell;NativeFunction 的正式名称是 EndorNativeFunctionObject。EndorBaseExceptionObject 保存异常数据及链,不建立类型继承或异常执行流程。EndorStringObject 使用 UTF-8,按 Unicode 标量值访问字符。更详细的接口和限制见 [基本对象](endor-builtins.md) 与 [基础对象](endor-object.md)。 ## 宿主与持久化 diff --git a/docs/development.md b/docs/development.md index 7cfcc25..eb3729a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -8,7 +8,9 @@ 用户已澄清:当前实现 Endor 的 VM 运行时,不实现 Python type 系统,也不定义语言根类。旧的 Class/Type、create_class、create_instance、root_class 和 type_of API 已移除。公共结构和操作接口在 object.rs,分配、引用计数和循环回收在 heap.rs,对外检查入口在 vm.rs。详见 endor-object.md。 -关键所有权规则:分配给一个外部强引用;句柄拷贝与 Value 克隆只产生借用;额外持有使用 retain。托管属性和 trace 声明的不可变载荷边分别计数。get_attr/call 返回借用值。长链释放使用迭代队列;循环回收以外部引用为根。添加可变载荷引用时必须设计 VM 管理的更新协议,不能只改 trace 的返回结果。 +关键所有权规则:分配给一个外部强引用;句柄拷贝与 Value 克隆只产生借用;额外持有使用 retain。托管属性和 trace 声明的载荷边分别计数。get_attr/call/dict_get/cell_get 返回借用值。长链释放使用迭代队列;循环回收以外部引用为根。内置字典和 Cell 已使用 VM 管理的载荷替换协议:先验证、保留新边,再更新载荷及快照,最后释放旧边;自定义载荷不能只改 trace 的返回结果。 + +基础载荷见 builtins.rs,统一无损 Number 见 number.rs,容器 API 和函数/异常关联校验见 vm_builtins.rs。功能与未实现边界详见 endor-builtins.md。新增 builtins 测试交由远端 CI 执行,不在本机运行;数字只完成表示,语言函数只完成环境数据,异常只完成实例数据,不能在交接里称执行器或数值引擎已完成。 CLI bootstrap 演示两个原生运行时对象并显式释放;HTTP /bootstrap 返回 object_model=EndorObject、type_system=false、heap_objects=0。修改宿主表示时同步 scripts/smoke.py。 diff --git a/docs/endor-builtins.md b/docs/endor-builtins.md new file mode 100644 index 0000000..10b7f1b --- /dev/null +++ b/docs/endor-builtins.md @@ -0,0 +1,129 @@ +# Endor 基本对象与统一 Number + +本次增加的是虚拟机实现层的数据结构与宿主 API,不增加 Python type、语言根类、解析器或执行器。它们通过 `ObjectPayload` 接入现有 `EndorObject`,共同使用对象头、稳定 Box、代际句柄、引用计数和循环回收。不是每种对象重新实现一套堆管理。 + +## 对应关系 + +| CPython 中的结构 | Endor 对应结构 | 当前实现 | +| --- | --- | --- | +| `PyFunctionObject` | `EndorFunctionObject` | 名称、代码数据、全局字典、默认参数、闭包 Cell;验证和保活关联对象 | +| `PyDictObject` | `EndorDictObject` | 保留插入顺序的键值容器,VM 管理查询、写入、删除和清空 | +| `PyCFunctionObject` | `EndorNativeFunctionObject` | Rust 宿主函数指针、名称与调用入口;旧名 NativeFunction 保留为别名 | +| `PyBaseExceptionObject` | `EndorBaseExceptionObject` | 异常名称、参数、cause、context、上下文抑制、备注与回溯位置 | +| `PyASCIIObject` 等 Unicode 布局 | `EndorStringObject` | 统一 UTF-8 字符串,支持字符数、字符读取、表示、真值、比较与哈希 | +| `PyLongObject`、`PyFloatObject` 等 | `EndorNumberObject` / `EndorNumber` | 一个对外数字接口,内部保留不同数字的无损表示 | +| `PyCellObject` | `EndorCellObject` | 可被多个闭包共享的可变槽;区分空槽与 Nil | + +`EndorCode` 是普通 Rust 代码数据结构,不是另一个堆对象。保存不透明的字节序列、常量、位置参数名和自由变量名。尚未定义指令集,所以字节序列不是 Python 字节码,也没有可执行性保证。 + +## Function 与 NativeFunction + +Python 代码中的 `def greet(name): ...` 创建的是语言函数。函数需要关联代码,以及代码执行时使用的全局环境、默认参数和闭包。`EndorFunctionObject` 保存这些信息;分配时验证 globals 是字典、closure 中每一项是 Cell,并检查默认参数数量、闭包数量和代码的参数信息一致。 + +CPython 的 `len`、`print` 等宿主实现的内建函数则通过 C 函数入口调用。Endor 使用 Rust,所以命名为 `EndorNativeFunctionObject`,不把 C 写入名称。 + +```rust +use aster_core::{EndorNativeFunctionObject, Value, Vm, VmError}; + +fn echo(arguments: &[Value]) -> Result { + match arguments { + [value] => Ok(value.clone()), + _ => Err(VmError::InvalidArgument("expected one value")), + } +} + +let mut vm = Vm::new()?; +let function = vm.allocate(EndorNativeFunctionObject::new("echo", echo))?; +let value = vm.call(function, &[Value::Text("hello".into())])?; +vm.release(function)?; +# Ok::<(), VmError>(()) +``` + +Native 函数现在可调用;语言函数的 `call` 明确返回 `UnsupportedOperation("language function execution")`。不以调用一个 Rust 回调来冒充语言函数执行。Native 回调仍接收借用参数、返回借用对象值,不能在回调里修改当前 VM。完整调用帧、参数绑定及执行器是后续工作。 + +## Number:优先表示,不先做重计算 + +原来的 `Value::Integer` / `Value::Float` 合并为 `Value::Number(EndorNumber)`。宿主仍可直接保存数字值;需要对象身份时才使用 `vm.new_number(number)` 装箱成 `EndorNumberObject`。 + +| 内部表示 | 适用数据 | 保留的内容 | +| --- | --- | --- | +| Integer | 超大整数、计数、编号 | 任意长度十进制数字和符号;去掉多余前导零,零统一为非负 | +| Decimal | 金额、精确小数、科学计数 | 原始数字、尾随零、正负号和任意长度十进制指数;完全不经过 f64 | +| Rational | 比例、分数 | 任意长度整数分子与非零分母;统一分母为正,不自动约分 | +| BinaryFloat | 宿主已有的 IEEE-754 数值 | 全部 64 位,包括负零、无穷、NaN 及其 payload | +| Complex | 复数记录 | 实部与虚部各自的 Integer、Decimal、Rational 或 BinaryFloat 表示 | + +```rust +use aster_core::{EndorNumber, Value}; + +let id = EndorNumber::integer("1234567890123456789012345678901234567890")?; +let amount = EndorNumber::decimal("12345678901234567890.000100")?; +let ratio = EndorNumber::rational("1", "3")?; +let tiny = EndorNumber::decimal("1e-999999999999999999999999999999")?; +let host_float = EndorNumber::binary_float(f64::INFINITY); +let value = Value::Number(amount); +# Ok::<(), aster_core::VmError>(()) +``` + +`EndorInteger`、`EndorDecimal`、`EndorRational` 是内部表示结构,不是三种语言数字类。构造器验证输入;分数不能有零分母;十进制指数不转换为机器整数,所以不会因指数很长而溢出。构造任意大整数或精确小数时传文本;已经被宿主转换为 f64 而丢失的精度无法恢复。整数的 `as_i64` 是显式、可失败的窄化读取,不会静默截断。 + +首版不实现四则运算、排序、跨表示数值相等和数值哈希。Rust 的 PartialEq / Eq 只检查表示是否相同,不能用于解释语言的 `==`:`1.0` 与 `1.00` 保留不同小数位数;`2/6` 与 `1/3` 尚未求数值等价;NaN 位模式也被保留。VM 对两个数字对象的数值比较与哈希显式报不支持。因此数字现在可以作为字典的值,暂不能作为字典键,避免先定义错误的键相等规则。没有用“全部转换 f64”的方式补齐这些操作。 + +这里的“各类数字”指上述五种可表示形式,不包括符号表达式、无限展开的精确 π、带单位数量、任意位宽二进制浮点等全部数学对象;这些可在未来扩展表示协议。 + +## Dict 与 Cell 的引用所有权 + +字典使用 `Vec<(Value, Value)>` 保留插入顺序,查询目前为 O(n),没有假装已经实现 CPython 哈希表性能。键须通过哈希能力检查;支持 Nil、Bool、文本及可哈希的对象。堆字符串与直接文本按内容匹配;覆盖已有键保留首次插入的键对象和位置。可变字典、Cell 与尚未定义数值哈希的数字不能当键。 + +`dict_items`、`dict_get`、`cell_get` 返回借用内容。宿主需要跨越删除、覆盖或容器释放继续持有一个对象时,应先 `retain`。删除字典条目不返回一个可能已经被回收的借用对象,而是返回是否删除成功。 + +字典键和值、Cell 内容、函数环境与闭包、异常参数与因果链都作为强引用参与 GC。可变容器不暴露内部可变引用:VM 先构造新载荷,验证并保留所有新边,再替换载荷和引用快照,最后释放旧边。对象自身的 Box、身份、属性和外部引用数不变。非法/跨 VM 句柄使更新失败,原值及引用计数不变。同对象替换不会在中间短暂释放它,自环、字典与 Cell 互相引用、函数与全局字典的环都能由现有循环回收处理。 + +## Cell 为什么存在 + +```python +def outer(): + x = "first" + def read(): + return x + def write(value): + nonlocal x + x = value + return read, write + +read, write = outer() +write("updated") +print(read()) # updated +``` + +`read` 与 `write` 必须引用同一个变量位置。若各自复制一份 `x` 的初始值,write 重新赋值后 read 就看不到变化。Cell 是这个共享位置:函数持有 Cell,Cell 持有当前值。外层函数返回后,Cell 仍被闭包持有。 + +```rust +use aster_core::{Value, Vm}; +let mut vm = Vm::new()?; +let cell = vm.new_cell(None)?; // 空槽,不是 Nil。 +vm.cell_set(cell, Value::Text("first".into()))?; +vm.cell_set(cell, Value::Text("updated".into()))?; +assert_eq!(vm.cell_get(cell)?, Value::Text("updated".into())); +vm.cell_clear(cell)?; +vm.release(cell)?; +# Ok::<(), aster_core::VmError>(()) +``` + +当前已实现 Cell 共享与保活的数据基础;上面的 Python 闭包示例说明语义目标,不表示 Endor 已能解析和执行它。 + +## 字符串与异常的准确含义 + +`PyASCIIObject` 是 CPython Unicode 字符串实现的公共头部,不是 Python 中另外一种 ASCII 字符串类。Endor 不复刻其紧凑布局,统一用 `EndorStringObject` 表示字符串。索引按 Unicode 标量值,不按 UTF-8 字节,也不按人眼字形簇。Rust String 不接受孤立代理码点,所以并非完整复制 Python str 的所有边界行为。repr 用 Rust 的转义文本显示,不承诺 Python repr 的逐字兼容。 + +Python 语言层所有异常的顶级基类叫 `BaseException`,其下除了普通 `Exception` 分支,还有 `SystemExit`、`KeyboardInterrupt` 等。`PyBaseExceptionObject` 是这些异常的公共 C 实例数据布局,不能把这个结构名直接当成语言类名。 + +Endor 当前用 `EndorBaseExceptionObject` 保存公共数据,名称只是错误标签;尚无异常继承和按类匹配机制,也没有 raise / try / except 执行流程。回溯保存独立的位置记录,不持有尚不存在的调用帧。宿主 API 的 `VmError` 仍表示操作失败,不会自动分配语言异常或把全部错误强制变成堆对象。 + +来源:[CPython 函数对象](https://docs.python.org/3.14/c-api/function.html)、[Cell 对象](https://docs.python.org/3.14/c-api/cell.html)、[异常说明](https://docs.python.org/3.14/library/exceptions.html)、[Unicode 对象](https://docs.python.org/3.14/c-api/unicode.html)。实现不承诺 CPython ABI 或默认 Python 兼容性。 + +## 验证状态 + +新增 `crates/aster-core/tests/builtins.rs`,检查数字无损表示、非法输入、字符串内容语义、字典顺序和引用所有权、更新失败的原子性、Cell 共享、循环回收、函数环境及异常链。已有 runtime 测试和 rustdoc 示例迁移到统一 Number。 + +遵守本机禁测约定:未在本地运行测试、构建或冒烟脚本;未下载依赖。远端结果必须以本次代码对应的 ces-control CI 为准,未推送前不标记通过。HTML 研究表仅为工作区外阅读材料,不提交 Git。 diff --git a/docs/endor-object.md b/docs/endor-object.md index 9cc1db2..0fe42a5 100644 --- a/docs/endor-object.md +++ b/docs/endor-object.md @@ -2,6 +2,8 @@ 当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。这里没有语言根类、元类、继承或 Python type 系统。标量仍然可以直接保存在 Value 中,不要求一切装箱。 +基本对象及统一数字表示已在此基础上加入,详见 [基本对象与 Number](endor-builtins.md)。`builtins.rs` 定义字符串、字典、Cell、函数和异常数据;`number.rs` 定义无损数字表示;`vm_builtins.rs` 提供容器操作及关联对象校验。 + ## 代码分布 | 文件 | 职责 | @@ -27,7 +29,7 @@ EndorObject `attributes` 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。 -`payload_edges` 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。分配后这些载荷边必须保持不变;需要修改的对象关系放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。 +`payload_edges` 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。自定义载荷分配后不能自行修改这些边。内置字典和 Cell 通过 VM 的载荷替换协议更新:验证和保留新边,替换载荷与快照,最后释放旧边;对象 Box 保持稳定。其他可变对象关系也可放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。 ## 指针与句柄 @@ -80,7 +82,7 @@ ObjectPayload 提供以下默认行为,载荷可以逐项覆盖: Record 使用身份语义并开启属性。NativeFunction 保存名称和 Rust 函数指针,覆盖 repr 和 call。原生函数收到 Value 参数并返回 Result;VM 在调用前后检查参数和返回值中的对象句柄。 -原生调用当前不能修改 VM 或直接在该 VM 中分配新对象;对象参数和对象返回值是借用句柄。这个接口足够表达标量计算和已有对象返回,还没有语言执行栈、函数帧、闭包或字节码执行。 +原生调用当前不能修改 VM 或直接在该 VM 中分配新对象;对象参数和对象返回值是借用句柄。这个接口足够表达标量计算和已有对象返回。函数数据与闭包 Cell 已具备,语言执行栈、函数执行帧和字节码执行尚未实现。 compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。它不实现 Python 的子类型优先规则。内容比较由扩展定义;如果两个对象内容相等,它们必须给出相同哈希,或者明确拒绝哈希。