Add Endor builtin objects and lossless unified number representations
Verify databases and deploy Aster VM / verify-and-deploy (push) Waiting to run
Verify databases and deploy Aster VM / verify-and-deploy (push) Waiting to run
This commit is contained in:
@@ -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)。
|
||||
|
||||
|
||||
@@ -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<String>) -> 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<char> {
|
||||
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<String, VmError> {
|
||||
Ok(format!("{:?}", self.text))
|
||||
}
|
||||
fn str(&self, _identity: ObjectRef) -> Result<String, VmError> {
|
||||
Ok(self.text.clone())
|
||||
}
|
||||
fn is_true(&self) -> Result<bool, VmError> {
|
||||
Ok(!self.text.is_empty())
|
||||
}
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.text.hash(&mut hasher);
|
||||
Ok(hasher.finish())
|
||||
}
|
||||
fn compare(&self, other: &dyn ObjectPayload, op: CompareOp) -> Result<Option<bool>, VmError> {
|
||||
let Some(other) = other.as_any().downcast_ref::<Self>() 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<String, VmError> {
|
||||
Ok(format!("<dict len={}>", self.len()))
|
||||
}
|
||||
fn is_true(&self) -> Result<bool, VmError> {
|
||||
Ok(!self.is_empty())
|
||||
}
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
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<Value>,
|
||||
}
|
||||
|
||||
impl EndorCellObject {
|
||||
pub fn new(value: Option<Value>) -> 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<String, VmError> {
|
||||
Ok(if self.value.is_some() {
|
||||
"<cell occupied>"
|
||||
} else {
|
||||
"<cell empty>"
|
||||
}
|
||||
.into())
|
||||
}
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
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<u8>,
|
||||
constants: Vec<Value>,
|
||||
parameters: Vec<String>,
|
||||
free_variables: Vec<String>,
|
||||
}
|
||||
|
||||
impl EndorCode {
|
||||
pub fn new(
|
||||
bytecode: Vec<u8>,
|
||||
constants: Vec<Value>,
|
||||
parameters: Vec<String>,
|
||||
free_variables: Vec<String>,
|
||||
) -> Result<Self, VmError> {
|
||||
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<Value>,
|
||||
closure: Vec<ObjectRef>,
|
||||
}
|
||||
|
||||
impl EndorFunctionObject {
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
code: EndorCode,
|
||||
globals: ObjectRef,
|
||||
defaults: Vec<Value>,
|
||||
closure: Vec<ObjectRef>,
|
||||
) -> Result<Self, VmError> {
|
||||
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<String, VmError> {
|
||||
Ok(format!("<function {}>", self.name))
|
||||
}
|
||||
fn call(&self, _arguments: &[Value]) -> Result<Value, VmError> {
|
||||
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<Value>,
|
||||
cause: Option<ObjectRef>,
|
||||
context: Option<ObjectRef>,
|
||||
suppress_context: bool,
|
||||
notes: Vec<String>,
|
||||
traceback: Vec<EndorTracebackEntry>,
|
||||
}
|
||||
|
||||
impl EndorBaseExceptionObject {
|
||||
pub fn new(name: impl Into<String>, args: Vec<Value>) -> 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<String>) -> 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<ObjectRef> {
|
||||
self.cause
|
||||
}
|
||||
pub fn context(&self) -> Option<ObjectRef> {
|
||||
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<String, VmError> {
|
||||
Ok(format!(
|
||||
"{}({})",
|
||||
self.name,
|
||||
self.args
|
||||
.iter()
|
||||
.map(|arg| format!("{arg:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
))
|
||||
}
|
||||
fn str(&self, _identity: ObjectRef) -> Result<String, VmError> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<Self, VmError> {
|
||||
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<i64> {
|
||||
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<Self, VmError> {
|
||||
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<Self, VmError> {
|
||||
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<Self, VmError> {
|
||||
Ok(Self::Real(EndorReal::Integer(EndorInteger::parse(text)?)))
|
||||
}
|
||||
|
||||
pub fn decimal(text: &str) -> Result<Self, VmError> {
|
||||
Ok(Self::Real(EndorReal::Decimal(EndorDecimal::parse(text)?)))
|
||||
}
|
||||
|
||||
pub fn rational(numerator: &str, denominator: &str) -> Result<Self, VmError> {
|
||||
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<i64> {
|
||||
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<i64> for EndorNumber {
|
||||
fn from(value: i64) -> Self {
|
||||
Self::integer(&value.to_string()).expect("i64 always has valid decimal digits")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> 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<String, VmError> {
|
||||
Ok(self.value.to_string())
|
||||
}
|
||||
|
||||
fn is_true(&self) -> Result<bool, VmError> {
|
||||
Ok(!self.value.is_zero())
|
||||
}
|
||||
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
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"));
|
||||
}
|
||||
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())
|
||||
}
|
||||
@@ -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<Value, VmError>;
|
||||
pub type EndorNativeCall = fn(&[Value]) -> Result<Value, VmError>;
|
||||
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<String>, 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);
|
||||
|
||||
@@ -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<ObjectRef, VmError> {
|
||||
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)?;
|
||||
}
|
||||
|
||||
@@ -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::<EndorFunctionObject>() {
|
||||
self.payload::<EndorDictObject>(function.globals(), "dict")?;
|
||||
for cell in function.closure() {
|
||||
self.payload::<EndorCellObject>(*cell, "cell")?;
|
||||
}
|
||||
}
|
||||
if let Some(exception) = payload.as_any().downcast_ref::<EndorBaseExceptionObject>() {
|
||||
for reference in [exception.cause(), exception.context()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
self.payload::<EndorBaseExceptionObject>(reference, "exception")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn payload<T: ObjectPayload>(
|
||||
&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<String>) -> Result<ObjectRef, VmError> {
|
||||
self.allocate(EndorStringObject::new(text))
|
||||
}
|
||||
|
||||
pub fn new_number(&mut self, number: EndorNumber) -> Result<ObjectRef, VmError> {
|
||||
self.allocate(EndorNumberObject::new(number))
|
||||
}
|
||||
|
||||
pub fn new_dict(&mut self) -> Result<ObjectRef, VmError> {
|
||||
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::<EndorDictObject>(dictionary, "dict")?
|
||||
.entries())
|
||||
}
|
||||
|
||||
pub fn dict_get(&self, dictionary: ObjectRef, key: &Value) -> Result<Option<Value>, VmError> {
|
||||
let dict = self.payload::<EndorDictObject>(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::<EndorDictObject>(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<bool, VmError> {
|
||||
let mut dict = self.payload::<EndorDictObject>(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::<EndorDictObject>(dictionary, "dict")?;
|
||||
self.heap
|
||||
.replace_payload(dictionary, EndorDictObject::new())
|
||||
}
|
||||
|
||||
fn dict_position(&self, dict: &EndorDictObject, key: &Value) -> Result<Option<usize>, 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<u64, VmError> {
|
||||
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<bool, VmError> {
|
||||
let text = |value: &Value| -> Result<Option<String>, VmError> {
|
||||
Ok(match value {
|
||||
Value::Text(text) => Some(text.clone()),
|
||||
Value::Object(reference) => self
|
||||
.object(*reference)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.downcast_ref::<EndorStringObject>()
|
||||
.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<Value>) -> Result<ObjectRef, VmError> {
|
||||
self.allocate(EndorCellObject::new(value))
|
||||
}
|
||||
|
||||
/// Reads a borrowed value; an empty cell is not Value::Nil.
|
||||
pub fn cell_get(&self, cell: ObjectRef) -> Result<Value, VmError> {
|
||||
self.payload::<EndorCellObject>(cell, "cell")?
|
||||
.value()
|
||||
.cloned()
|
||||
.ok_or(VmError::EmptyCell)
|
||||
}
|
||||
|
||||
pub fn cell_set(&mut self, cell: ObjectRef, value: Value) -> Result<(), VmError> {
|
||||
self.payload::<EndorCellObject>(cell, "cell")?;
|
||||
self.heap
|
||||
.replace_payload(cell, EndorCellObject::new(Some(value)))
|
||||
}
|
||||
|
||||
pub fn cell_clear(&mut self, cell: ObjectRef) -> Result<(), VmError> {
|
||||
self.payload::<EndorCellObject>(cell, "cell")?;
|
||||
self.heap.replace_payload(cell, EndorCellObject::default())
|
||||
}
|
||||
}
|
||||
@@ -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::<EndorNumberObject>(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::<EndorStringObject>(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::<EndorFunctionObject>(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::<EndorBaseExceptionObject>(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<Value, VmError> {
|
||||
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::<EndorDictObject>(dict, "dict").unwrap().len(),
|
||||
0
|
||||
);
|
||||
}
|
||||
@@ -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<Value, VmError> {
|
||||
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(), "<native-function add>");
|
||||
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!(
|
||||
|
||||
@@ -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)。
|
||||
|
||||
## 宿主与持久化
|
||||
|
||||
|
||||
+3
-1
@@ -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。
|
||||
|
||||
|
||||
@@ -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<Value, VmError> {
|
||||
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。
|
||||
@@ -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<Value, VmError>;VM 在调用前后检查参数和返回值中的对象句柄。
|
||||
|
||||
原生调用当前不能修改 VM 或直接在该 VM 中分配新对象;对象参数和对象返回值是借用句柄。这个接口足够表达标量计算和已有对象返回,还没有语言执行栈、函数帧、闭包或字节码执行。
|
||||
原生调用当前不能修改 VM 或直接在该 VM 中分配新对象;对象参数和对象返回值是借用句柄。这个接口足够表达标量计算和已有对象返回。函数数据与闭包 Cell 已具备,语言执行栈、函数执行帧和字节码执行尚未实现。
|
||||
|
||||
compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。它不实现 Python 的子类型优先规则。内容比较由扩展定义;如果两个对象内容相等,它们必须给出相同哈希,或者明确拒绝哈希。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user