refactor: extract core objects into a standalone package
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s

This commit is contained in:
2026-10-19 13:33:24 +08:00
parent 78fc859825
commit f77b8370ea
40 changed files with 631 additions and 202 deletions
+20
View File
@@ -0,0 +1,20 @@
# Objects 源码导航
公开导出集中在 lib.rs;本目录只负责对象与执行数据,不包含 VM 调度或取指分派。全局契约见 [运行时文档](../../../docs/runtime/README.md)。
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出与独立存储 rustdoc 示例 |
| [object.rs](object.rs) | Value、CompareOp、ObjectPayload、ObjectHeader、EndorObject、Record、NativeFunction |
| [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 |
| [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边 |
| [heap.rs](heap.rs) | 私有 Heap、不可伪造 ObjectRef、稳定 Box、引用事务、迭代释放、循环回收及私有边界测试 |
| [store.rs](store.rs) | ObjectStore、空堆创建、对象操作、retain/release 和回收入口 |
| [store_builtins.rs](store_builtins.rs) | 载荷校验、代码/函数工厂、Dict/Cell 操作及键语义 |
| [instruction.rs](instruction.rs) | Opcode、Instruction、wordcode 编解码、MakeFunction/比较参数与跳转读取接口 |
| [code.rs](code.rs) | CodeLayout、EndorCode/EndorCodeObject、共享不可变代码和操作数/控制流/栈验证 |
| [error.rs](error.rs) | 对象、代码与执行共用的 VmError,避免反向依赖 core |
对象操作变更同步 [对象模型](../../../docs/runtime/objects.md),内置载荷同步 [基本对象](../../../docs/runtime/builtins.md)。新增操作码需要同时修改本包 instruction.rs/code.rs 和 core 的 eval.rs 分派,见 [字节码 VM](../../../docs/runtime/bytecode.md)。
Heap、ObjectRef 字段、对象头计数和可变容器字段不向包外开放;公开代码读取接口不允许改写已登记的常量和托管边。检查入口见 [tests](../tests/README.md)。返回 [objects](../README.md)。
+347
View File
@@ -0,0 +1,347 @@
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"))
}
}
#[derive(Debug, Clone)]
pub struct EndorFunctionObject {
name: String,
code: ObjectRef,
builtins: Option<ObjectRef>,
globals: ObjectRef,
defaults: Vec<Value>,
closure: Vec<ObjectRef>,
}
impl EndorFunctionObject {
pub(crate) fn new(
name: String,
code: ObjectRef,
globals: ObjectRef,
builtins: Option<ObjectRef>,
defaults: Vec<Value>,
closure: Vec<ObjectRef>,
) -> Self {
Self {
name,
code,
globals,
builtins,
defaults,
closure,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn code(&self) -> ObjectRef {
self.code
}
pub fn builtins(&self) -> Option<ObjectRef> {
self.builtins
}
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.code);
visit(self.globals);
if let Some(builtins) = self.builtins {
visit(builtins);
}
for reference in &self.closure {
visit(*reference);
}
for value in &self.defaults {
trace_value(value, visit);
}
}
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
Ok(format!("<function {}>", self.name))
}
}
#[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);
}
}
+389
View File
@@ -0,0 +1,389 @@
use std::any::Any;
use std::collections::{BTreeSet, VecDeque};
use std::sync::Arc;
use crate::builtins::trace_value;
use crate::instruction::{compare_operation, decode, function_counts, invalid};
use crate::{DecodedInstruction, Instruction, ObjectPayload, ObjectRef, Opcode, Value, VmError};
/// Local slots start with positional parameters. Captured locals occupy their
/// original slots as cells; free-variable cells follow all local slots.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodeLayout {
pub parameters: Vec<String>,
pub locals: Vec<String>,
pub cell_variables: Vec<String>,
pub free_variables: Vec<String>,
pub names: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct EndorCode {
bytecode: Vec<u8>,
instructions: Vec<DecodedInstruction>,
constants: Vec<Value>,
layout: CodeLayout,
stack_size: usize,
name: String,
filename: String,
first_line: usize,
}
impl EndorCode {
pub fn new(
bytecode: Vec<u8>,
constants: Vec<Value>,
parameters: Vec<String>,
free_variables: Vec<String>,
) -> Result<Self, VmError> {
Self::with_layout(
bytecode,
constants,
CodeLayout {
locals: parameters.clone(),
parameters,
free_variables,
..CodeLayout::default()
},
)
}
pub fn from_instructions(
instructions: &[Instruction],
constants: Vec<Value>,
layout: CodeLayout,
) -> Result<Self, VmError> {
Self::with_layout(Instruction::encode_all(instructions)?, constants, layout)
}
pub fn with_layout(
bytecode: Vec<u8>,
constants: Vec<Value>,
layout: CodeLayout,
) -> Result<Self, VmError> {
validate_layout(&layout)?;
let instructions = decode(&bytecode)?;
let stack_size = verify(&instructions, constants.len(), &layout)?;
Ok(Self {
bytecode,
instructions,
constants,
layout,
stack_size,
name: "<code>".into(),
filename: "<endor>".into(),
first_line: 1,
})
}
pub fn with_location(
mut self,
name: impl Into<String>,
filename: impl Into<String>,
first_line: usize,
) -> Self {
self.name = name.into();
self.filename = filename.into();
self.first_line = first_line;
self
}
pub fn bytecode(&self) -> &[u8] {
&self.bytecode
}
pub fn instructions(&self) -> &[DecodedInstruction] {
&self.instructions
}
pub fn constants(&self) -> &[Value] {
&self.constants
}
pub fn parameters(&self) -> &[String] {
&self.layout.parameters
}
pub fn free_variables(&self) -> &[String] {
&self.layout.free_variables
}
pub fn layout(&self) -> &CodeLayout {
&self.layout
}
pub fn stack_size(&self) -> usize {
self.stack_size
}
pub fn name(&self) -> &str {
&self.name
}
pub fn filename(&self) -> &str {
&self.filename
}
pub fn first_line(&self) -> usize {
self.first_line
}
pub fn slot_count(&self) -> usize {
self.layout.locals.len() + self.layout.free_variables.len()
}
/// Whether a local/free slot holds a managed closure cell.
pub fn is_cell_slot(&self, slot: usize) -> bool {
is_cell_slot(&self.layout, slot)
}
/// Name of a local/free slot used by validated instructions.
///
/// # Panics
/// Panics if the slot is outside this code's local/free-variable layout.
pub fn slot_name(&self, slot: usize) -> &str {
if slot < self.layout.locals.len() {
&self.layout.locals[slot]
} else {
&self.layout.free_variables[slot - self.layout.locals.len()]
}
}
/// Look up a logical instruction by its code-unit offset.
pub fn instruction_at(&self, offset: usize) -> Result<DecodedInstruction, VmError> {
instruction_index(&self.instructions, offset)
.map(|index| self.instructions[index])
.ok_or_else(|| {
invalid(
offset,
"instruction pointer is not on an instruction boundary",
)
})
}
}
/// Heap-allocated, immutable and shareable executable. Function objects hold a
/// managed reference to this object rather than embedding a copy of its code.
#[derive(Debug, Clone)]
pub struct EndorCodeObject {
code: Arc<EndorCode>,
}
impl EndorCodeObject {
pub fn new(code: EndorCode) -> Self {
Self {
code: Arc::new(code),
}
}
pub fn code(&self) -> &EndorCode {
&self.code
}
/// Share immutable executable data without copying constants or wordcode.
/// This Rust Arc does not retain heap objects referenced by constants;
/// callers must keep the managed code object rooted while using them.
pub fn shared_code(&self) -> Arc<EndorCode> {
Arc::clone(&self.code)
}
}
impl ObjectPayload for EndorCodeObject {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"code"
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
for value in self.code.constants() {
trace_value(value, visit);
}
}
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
Ok(format!(
"<code {} at {}:{}>",
self.code.name(),
self.code.filename(),
self.code.first_line()
))
}
}
fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
for names in [
&layout.parameters,
&layout.locals,
&layout.cell_variables,
&layout.free_variables,
&layout.names,
] {
let mut seen = BTreeSet::new();
for name in names {
if name.is_empty() || !seen.insert(name) {
return Err(VmError::InvalidArgument(
"empty or duplicate code variable name",
));
}
}
}
if !layout.locals.starts_with(&layout.parameters) {
return Err(VmError::InvalidArgument(
"parameter names must lead local slots",
));
}
if layout
.cell_variables
.iter()
.any(|name| !layout.locals.contains(name))
|| layout
.free_variables
.iter()
.any(|name| layout.locals.contains(name))
{
return Err(VmError::InvalidArgument(
"invalid local/cell/free-variable layout",
));
}
Ok(())
}
fn is_cell_slot(layout: &CodeLayout, slot: usize) -> bool {
if slot >= layout.locals.len() {
slot < layout.locals.len() + layout.free_variables.len()
} else {
layout.cell_variables.contains(&layout.locals[slot])
}
}
fn instruction_index(instructions: &[DecodedInstruction], offset: usize) -> Option<usize> {
instructions
.binary_search_by_key(&offset, |instruction| instruction.offset)
.ok()
}
pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result<usize, VmError> {
let argument = instruction.instruction.argument as usize;
let target = if instruction.instruction.opcode == Opcode::JumpBackward {
instruction.next_offset.checked_sub(argument)
} else {
instruction.next_offset.checked_add(argument)
};
target.ok_or_else(|| invalid(instruction.offset, "jump offset overflow"))
}
/// Validate every operand, then propagate stack heights through reachable
/// control flow. Bad jumps, underflow, mismatched joins and fallthrough fail
/// before execution; loops must return to the same stack height.
fn verify(
instructions: &[DecodedInstruction],
constants: usize,
layout: &CodeLayout,
) -> Result<usize, VmError> {
for instruction in instructions {
let Instruction { opcode, argument } = instruction.instruction;
let index = argument as usize;
let valid = match opcode {
Opcode::LoadConst => index < constants,
Opcode::LoadFast | Opcode::StoreFast | Opcode::DeleteFast => {
index < layout.locals.len() && !is_cell_slot(layout, index)
}
Opcode::LoadDeref | Opcode::StoreDeref | Opcode::DeleteDeref | Opcode::LoadClosure => {
is_cell_slot(layout, index)
}
Opcode::LoadGlobal
| Opcode::StoreGlobal
| Opcode::DeleteGlobal
| Opcode::LoadAttr
| Opcode::StoreAttr => index < layout.names.len(),
Opcode::CompareOp => compare_operation(argument).is_some(),
Opcode::Copy => argument >= 1,
Opcode::Swap => argument >= 2,
Opcode::JumpForward
| Opcode::JumpBackward
| Opcode::PopJumpIfFalse
| Opcode::PopJumpIfTrue => {
instruction_index(instructions, jump_target(*instruction)?).is_some()
}
Opcode::ExtendedArg => false,
_ => true,
};
if !valid {
return Err(invalid(
instruction.offset,
"invalid operand or jump target",
));
}
}
let mut heights = vec![None; instructions.len()];
heights[0] = Some(0_usize);
let mut pending = VecDeque::from([0]);
let mut maximum = 0;
while let Some(index) = pending.pop_front() {
let instruction = instructions[index];
let height = heights[index].unwrap();
let Instruction { opcode, argument } = instruction.instruction;
let argument = argument as usize;
let (required, popped, pushed) = match opcode {
Opcode::LoadConst
| Opcode::LoadFast
| Opcode::LoadGlobal
| Opcode::LoadDeref
| Opcode::LoadClosure
| Opcode::PushNull => (0, 0, 1),
Opcode::StoreFast
| Opcode::StoreGlobal
| Opcode::PopTop
| Opcode::StoreDeref
| Opcode::PopJumpIfFalse
| Opcode::PopJumpIfTrue => (1, 1, 0),
Opcode::ToBool | Opcode::UnaryNot | Opcode::LoadAttr => (1, 1, 1),
Opcode::CompareOp => (2, 2, 1),
Opcode::StoreAttr => (2, 2, 0),
Opcode::Copy => (argument, 0, 1),
Opcode::Swap => (argument, 0, 0),
Opcode::Call => {
let consumed = argument
.checked_add(2)
.ok_or_else(|| invalid(instruction.offset, "call operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::BuildMap => {
let consumed = argument
.checked_mul(2)
.ok_or_else(|| invalid(instruction.offset, "map operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::MakeFunction => {
let (defaults, closure) = function_counts(instruction.instruction.argument);
let consumed = defaults + closure + 1;
(consumed, consumed, 1)
}
Opcode::ReturnValue => (1, 1, 0),
_ => (0, 0, 0),
};
if height < required {
return Err(invalid(instruction.offset, "evaluation stack underflow"));
}
if opcode == Opcode::ReturnValue && height != 1 {
return Err(invalid(
instruction.offset,
"RETURN_VALUE must leave an empty frame stack",
));
}
let next_height = height - popped + pushed;
maximum = maximum.max(height).max(next_height);
let successors: Vec<usize> = match opcode {
Opcode::ReturnValue => Vec::new(),
Opcode::JumpForward | Opcode::JumpBackward => vec![jump_target(instruction)?],
Opcode::PopJumpIfFalse | Opcode::PopJumpIfTrue => {
vec![jump_target(instruction)?, instruction.next_offset]
}
_ => vec![instruction.next_offset],
};
for successor in successors {
let target = instruction_index(instructions, successor).ok_or_else(|| {
invalid(instruction.offset, "execution falls off the code object")
})?;
match heights[target] {
Some(existing) if existing != next_height => {
return Err(invalid(
successor,
"control-flow paths have different stack heights",
));
}
Some(_) => {}
None => {
heights[target] = Some(next_height);
pending.push_back(target);
}
}
}
}
Ok(maximum)
}
+66
View File
@@ -0,0 +1,66 @@
use std::fmt;
/// Shared object/bytecode/execution errors. The established VmError name is
/// retained for embedders; defining it here avoids a dependency on the VM.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VmError {
ForeignHandle,
InvalidHandle,
IdentityExhausted,
RefCountOverflow,
NoOwnedReference,
AttributeNotFound(String),
UnsupportedOperation(&'static str),
InvalidArgument(&'static str),
ObjectKindMismatch(&'static str),
EmptyCell,
InvalidBytecode {
offset: usize,
reason: &'static str,
},
UnboundLocal(String),
NameNotFound(String),
ArgumentCount {
name: String,
required: usize,
maximum: usize,
provided: usize,
},
ExecutionLimit(&'static str),
}
impl fmt::Display for VmError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ForeignHandle => f.write_str("handle belongs to another VM"),
Self::InvalidHandle => f.write_str("handle does not identify a live object"),
Self::IdentityExhausted => f.write_str("process-local VM identities exhausted"),
Self::RefCountOverflow => f.write_str("object reference count exhausted"),
Self::NoOwnedReference => f.write_str("no external strong reference to release"),
Self::AttributeNotFound(name) => write!(f, "attribute not found: {name}"),
Self::UnsupportedOperation(operation) => {
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"),
Self::InvalidBytecode { offset, reason } => {
write!(f, "invalid bytecode at code unit {offset}: {reason}")
}
Self::UnboundLocal(name) => write!(f, "local variable is unbound: {name}"),
Self::NameNotFound(name) => write!(f, "name not found: {name}"),
Self::ArgumentCount {
name,
required,
maximum,
provided,
} => write!(
f,
"{name} expects {required}..={maximum} positional arguments, received {provided}"
),
Self::ExecutionLimit(limit) => write!(f, "execution limit reached: {limit}"),
}
}
}
impl std::error::Error for VmError {}
+381
View File
@@ -0,0 +1,381 @@
use std::collections::BTreeMap;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::{EndorObject, ObjectHeader, ObjectPayload, Value, VmError};
static NEXT_HEAP_ID: AtomicU64 = AtomicU64::new(1);
/// Checked, process-local reference. Copying it never retains an object.
/// The generation prevents a released handle from aliasing a reused slot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectRef {
heap_id: u64,
index: usize,
generation: u64,
}
impl fmt::Display for ObjectRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.heap_id, self.index, self.generation)
}
}
#[derive(Debug)]
struct Slot {
generation: u64,
object: Option<Box<EndorObject>>,
}
#[derive(Debug)]
pub(crate) struct Heap {
id: u64,
slots: Vec<Slot>,
free: Vec<usize>,
live: usize,
}
impl Heap {
pub(crate) fn new() -> Result<Self, VmError> {
let id = NEXT_HEAP_ID
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
.map_err(|_| VmError::IdentityExhausted)?;
Ok(Self {
id,
slots: Vec::new(),
free: Vec::new(),
live: 0,
})
}
pub(crate) fn allocate(
&mut self,
payload: Box<dyn ObjectPayload>,
) -> Result<ObjectRef, VmError> {
let mut edges = Vec::new();
payload.trace(&mut |reference| edges.push(reference));
let increments = self.prepare_increments(&edges)?;
let index = self.free.pop().unwrap_or(self.slots.len());
if index == self.slots.len() {
self.slots.push(Slot {
generation: 0,
object: None,
});
}
let reference = ObjectRef {
heap_id: self.id,
index,
generation: self.slots[index].generation,
};
let object = Box::new(EndorObject {
header: ObjectHeader {
identity: reference,
strong_refs: 1,
external_refs: 1,
},
payload,
attributes: BTreeMap::new(),
payload_edges: edges,
});
self.apply_increments(increments);
self.slots[index].object = Some(object);
self.live += 1;
Ok(reference)
}
pub(crate) fn get(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> {
self.validate_owner(reference)?;
self.slots
.get(reference.index)
.filter(|slot| slot.generation == reference.generation)
.and_then(|slot| slot.object.as_deref())
.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())
}
fn validate_owner(&self, reference: ObjectRef) -> Result<(), VmError> {
if reference.heap_id != self.id {
return Err(VmError::ForeignHandle);
}
Ok(())
}
fn prepare_increments(
&self,
references: &[ObjectRef],
) -> Result<BTreeMap<ObjectRef, usize>, VmError> {
let mut counts = BTreeMap::<ObjectRef, usize>::new();
for reference in references {
self.get(*reference)?;
let count = counts.entry(*reference).or_default();
*count = count.checked_add(1).ok_or(VmError::RefCountOverflow)?;
}
for (reference, count) in &counts {
self.get(*reference)?
.header
.strong_refs
.checked_add(*count)
.ok_or(VmError::RefCountOverflow)?;
}
Ok(counts)
}
fn apply_increments(&mut self, increments: BTreeMap<ObjectRef, usize>) {
for (reference, count) in increments {
self.get_mut(reference).unwrap().header.strong_refs += count;
}
}
pub(crate) fn retain(&mut self, reference: ObjectRef) -> Result<(), VmError> {
let header = &mut self.get_mut(reference)?.header;
let strong = header
.strong_refs
.checked_add(1)
.ok_or(VmError::RefCountOverflow)?;
let external = header
.external_refs
.checked_add(1)
.ok_or(VmError::RefCountOverflow)?;
header.strong_refs = strong;
header.external_refs = external;
Ok(())
}
pub(crate) fn release(&mut self, reference: ObjectRef) -> Result<(), VmError> {
let header = &mut self.get_mut(reference)?.header;
if header.external_refs == 0 {
return Err(VmError::NoOwnedReference);
}
header.external_refs -= 1;
header.strong_refs -= 1;
let pending = if header.strong_refs == 0 {
vec![reference]
} else {
Vec::new()
};
self.drain_zero_counts(pending);
Ok(())
}
pub(crate) fn set_attr(
&mut self,
reference: ObjectRef,
name: String,
value: Value,
) -> Result<(), VmError> {
self.check_attributes(reference)?;
let edges: Vec<_> = match &value {
Value::Object(target) => vec![*target],
_ => Vec::new(),
};
let increments = self.prepare_increments(&edges)?;
self.apply_increments(increments);
let old = self.get_mut(reference)?.attributes.insert(name, value);
if let Some(Value::Object(target)) = old {
self.release_edges(vec![target]);
}
Ok(())
}
pub(crate) fn get_attr(&self, reference: ObjectRef, name: &str) -> Result<Value, VmError> {
self.check_attributes(reference)?;
self.get(reference)?
.attributes
.get(name)
.cloned()
.ok_or_else(|| VmError::AttributeNotFound(name.into()))
}
pub(crate) fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> {
self.check_attributes(reference)?;
let old = self
.get_mut(reference)?
.attributes
.remove(name)
.ok_or_else(|| VmError::AttributeNotFound(name.into()))?;
if let Value::Object(target) = old {
self.release_edges(vec![target]);
}
Ok(())
}
fn check_attributes(&self, reference: ObjectRef) -> Result<(), VmError> {
if !self.get(reference)?.payload.allows_attributes() {
return Err(VmError::UnsupportedOperation("attributes"));
}
Ok(())
}
fn release_edges(&mut self, edges: Vec<ObjectRef>) {
let mut pending = Vec::new();
for target in edges {
let header = &mut self.get_mut(target).unwrap().header;
header.strong_refs -= 1;
if header.strong_refs == 0 {
pending.push(target);
}
}
self.drain_zero_counts(pending);
}
/// Removes the object before running Drop. Reuse increments its generation;
/// an exhausted generation permanently retires that slot.
fn take(&mut self, reference: ObjectRef) -> Box<EndorObject> {
let slot = &mut self.slots[reference.index];
let object = slot.object.take().unwrap();
if let Some(next) = slot.generation.checked_add(1) {
slot.generation = next;
self.free.push(reference.index);
}
self.live -= 1;
object
}
fn drain_zero_counts(&mut self, mut pending: Vec<ObjectRef>) {
while let Some(reference) = pending.pop() {
let object = self.take(reference);
for target in object.edges() {
let header = &mut self.get_mut(target).unwrap().header;
header.strong_refs -= 1;
if header.strong_refs == 0 {
pending.push(target);
}
}
drop(object);
}
}
/// Full, explicit cycle collection. External references are roots; every
/// managed attribute and declared payload edge participates in tracing.
pub(crate) fn collect_cycles(&mut self) -> usize {
let mut marked = vec![false; self.slots.len()];
let mut pending = Vec::new();
for slot in &self.slots {
if let Some(object) = &slot.object
&& object.header.external_refs > 0
{
pending.push(object.header.identity);
}
}
while let Some(reference) = pending.pop() {
if marked[reference.index] {
continue;
}
marked[reference.index] = true;
pending.extend(self.get(reference).unwrap().edges());
}
let garbage: Vec<_> = self
.slots
.iter()
.enumerate()
.filter(|(index, _)| !marked[*index])
.filter_map(|(_, slot)| slot.object.as_ref().map(|object| object.header.identity))
.collect();
let objects: Vec<_> = garbage
.iter()
.map(|reference| self.take(*reference))
.collect();
// All garbage slots are detached before touching edges or running Drop.
for object in &objects {
for target in object.edges() {
if let Ok(survivor) = self.get_mut(target) {
survivor.header.strong_refs -= 1;
}
}
}
let collected = objects.len();
drop(objects);
collected
}
pub(crate) fn len(&self) -> usize {
self.live
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Record;
#[test]
fn invalid_slots_and_generations_are_rejected() {
let mut heap = Heap::new().unwrap();
let reference = heap.allocate(Box::new(Record)).unwrap();
let invalid = ObjectRef {
index: usize::MAX,
..reference
};
let stale = ObjectRef {
generation: 1,
..reference
};
assert!(matches!(heap.get(invalid), Err(VmError::InvalidHandle)));
assert!(matches!(heap.get_mut(stale), Err(VmError::InvalidHandle)));
}
#[test]
fn reused_slots_never_alias_and_exhausted_generations_retire() {
let mut heap = Heap::new().unwrap();
let first = heap.allocate(Box::new(Record)).unwrap();
heap.release(first).unwrap();
let second = heap.allocate(Box::new(Record)).unwrap();
assert_eq!(first.index, second.index);
assert_ne!(first.generation, second.generation);
assert!(matches!(heap.get(first), Err(VmError::InvalidHandle)));
heap.slots[second.index].generation = u64::MAX;
let last = ObjectRef {
generation: u64::MAX,
..second
};
heap.slots[last.index]
.object
.as_mut()
.unwrap()
.header
.identity = last;
heap.release(last).unwrap();
let next = heap.allocate(Box::new(Record)).unwrap();
assert_ne!(next.index, last.index);
}
#[test]
fn failed_reference_increments_leave_counts_and_attributes_unchanged() {
let mut heap = Heap::new().unwrap();
let owner = heap.allocate(Box::new(Record)).unwrap();
let target = heap.allocate(Box::new(Record)).unwrap();
heap.get_mut(target).unwrap().header.strong_refs = usize::MAX;
assert_eq!(heap.retain(target), Err(VmError::RefCountOverflow));
assert_eq!(heap.get(target).unwrap().header.external_refs, 1);
assert_eq!(
heap.set_attr(owner, "edge".into(), Value::Object(target)),
Err(VmError::RefCountOverflow)
);
assert!(heap.get(owner).unwrap().attributes.is_empty());
}
}
+246
View File
@@ -0,0 +1,246 @@
use crate::{CompareOp, VmError};
/// Endor wordcode, following CPython's opcode/oparg code-unit design.
/// Numeric opcode IDs are Endor's own, not a CPython bytecode ABI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Opcode {
Nop = 0,
LoadConst = 1,
LoadFast = 2,
StoreFast = 3,
DeleteFast = 4,
LoadGlobal = 5,
StoreGlobal = 6,
DeleteGlobal = 7,
PopTop = 8,
Copy = 9,
Swap = 10,
PushNull = 11,
Call = 12,
ReturnValue = 13,
JumpForward = 14,
JumpBackward = 15,
PopJumpIfFalse = 16,
PopJumpIfTrue = 17,
ToBool = 18,
UnaryNot = 19,
CompareOp = 20,
LoadDeref = 21,
StoreDeref = 22,
DeleteDeref = 23,
LoadClosure = 24,
LoadAttr = 25,
StoreAttr = 26,
BuildMap = 27,
MakeFunction = 28,
ExtendedArg = 255,
}
impl Opcode {
fn decode(byte: u8) -> Option<Self> {
Some(match byte {
0 => Self::Nop,
1 => Self::LoadConst,
2 => Self::LoadFast,
3 => Self::StoreFast,
4 => Self::DeleteFast,
5 => Self::LoadGlobal,
6 => Self::StoreGlobal,
7 => Self::DeleteGlobal,
8 => Self::PopTop,
9 => Self::Copy,
10 => Self::Swap,
11 => Self::PushNull,
12 => Self::Call,
13 => Self::ReturnValue,
14 => Self::JumpForward,
15 => Self::JumpBackward,
16 => Self::PopJumpIfFalse,
17 => Self::PopJumpIfTrue,
18 => Self::ToBool,
19 => Self::UnaryNot,
20 => Self::CompareOp,
21 => Self::LoadDeref,
22 => Self::StoreDeref,
23 => Self::DeleteDeref,
24 => Self::LoadClosure,
25 => Self::LoadAttr,
26 => Self::StoreAttr,
27 => Self::BuildMap,
28 => Self::MakeFunction,
255 => Self::ExtendedArg,
_ => return None,
})
}
pub(crate) fn takes_argument(self) -> bool {
!matches!(
self,
Self::Nop
| Self::PopTop
| Self::PushNull
| Self::ReturnValue
| Self::ToBool
| Self::UnaryNot
)
}
}
/// A decoded logical instruction; EXTENDED_ARG is handled by the codec.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Instruction {
pub opcode: Opcode,
pub argument: u32,
}
impl Instruction {
pub const fn new(opcode: Opcode, argument: u32) -> Self {
Self { opcode, argument }
}
pub const fn simple(opcode: Opcode) -> Self {
Self::new(opcode, 0)
}
/// Build a function from [defaults..., closure cells..., code] on the stack.
/// The low 16 operand bits count trailing positional defaults; the high
/// 16 bits count cells in the code's free-variable order. No tuples or
/// subsequent mutation of the function object are required.
pub const fn make_function(defaults: u16, closure: u16) -> Self {
Self::new(
Opcode::MakeFunction,
(defaults as u32) | ((closure as u32) << 16),
)
}
pub const fn compare(operation: CompareOp) -> Self {
Self::new(
Opcode::CompareOp,
match operation {
CompareOp::Lt => 0,
CompareOp::Le => 1,
CompareOp::Eq => 2,
CompareOp::Ne => 3,
CompareOp::Gt => 4,
CompareOp::Ge => 5,
},
)
}
/// Decode a comparison operand; None denotes an invalid operand.
pub fn compare_operation(argument: u32) -> Option<CompareOp> {
compare_operation(argument)
}
/// Decode the default-value and closure-cell counts of MAKE_FUNCTION.
pub fn function_counts(argument: u32) -> (usize, usize) {
function_counts(argument)
}
/// Encodes 8-bit operands directly and prefixes up to three EXTENDED_ARG
/// code units for larger operands. Each code unit is [opcode, operand].
pub fn encode_all(instructions: &[Self]) -> Result<Vec<u8>, VmError> {
let mut bytes = Vec::new();
for instruction in instructions {
if instruction.opcode == Opcode::ExtendedArg {
return Err(invalid(0, "EXTENDED_ARG is emitted by the encoder"));
}
if !instruction.opcode.takes_argument() && instruction.argument != 0 {
return Err(invalid(bytes.len() / 2, "unexpected instruction operand"));
}
let parts = instruction.argument.to_be_bytes();
let first = parts.iter().position(|part| *part != 0).unwrap_or(3);
for part in &parts[first..3] {
bytes.extend([Opcode::ExtendedArg as u8, *part]);
}
bytes.extend([instruction.opcode as u8, parts[3]]);
}
Ok(bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodedInstruction {
/// Offsets count two-byte code units, including EXTENDED_ARG prefixes.
pub offset: usize,
pub next_offset: usize,
pub instruction: Instruction,
}
impl DecodedInstruction {
/// Resolve a jump relative to the end of this logical instruction.
pub fn jump_target(self) -> Result<usize, VmError> {
if !matches!(
self.instruction.opcode,
Opcode::JumpForward
| Opcode::JumpBackward
| Opcode::PopJumpIfFalse
| Opcode::PopJumpIfTrue
) {
return Err(invalid(self.offset, "instruction is not a jump"));
}
crate::code::jump_target(self)
}
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Vec<DecodedInstruction>, VmError> {
if bytes.is_empty() || !bytes.len().is_multiple_of(2) {
return Err(invalid(
0,
"wordcode must contain complete two-byte code units",
));
}
let mut instructions = Vec::new();
let mut cursor = 0;
while cursor < bytes.len() {
let start = cursor / 2;
let mut argument = 0_u32;
let mut prefixes = 0;
loop {
if cursor >= bytes.len() {
return Err(invalid(start, "dangling EXTENDED_ARG"));
}
let opcode = Opcode::decode(bytes[cursor])
.ok_or_else(|| invalid(cursor / 2, "unknown opcode"))?;
argument = (argument << 8) | u32::from(bytes[cursor + 1]);
cursor += 2;
if opcode == Opcode::ExtendedArg {
prefixes += 1;
if prefixes > 3 {
return Err(invalid(start, "too many EXTENDED_ARG prefixes"));
}
continue;
}
if !opcode.takes_argument() && (argument != 0 || prefixes != 0) {
return Err(invalid(start, "unexpected instruction operand"));
}
instructions.push(DecodedInstruction {
offset: start,
next_offset: cursor / 2,
instruction: Instruction::new(opcode, argument),
});
break;
}
}
Ok(instructions)
}
pub(crate) fn compare_operation(argument: u32) -> Option<CompareOp> {
Some(match argument {
0 => CompareOp::Lt,
1 => CompareOp::Le,
2 => CompareOp::Eq,
3 => CompareOp::Ne,
4 => CompareOp::Gt,
5 => CompareOp::Ge,
_ => return None,
})
}
pub(crate) fn function_counts(argument: u32) -> (usize, usize) {
((argument & 0xffff) as usize, (argument >> 16) as usize)
}
pub(crate) fn invalid(offset: usize, reason: &'static str) -> VmError {
VmError::InvalidBytecode { offset, reason }
}
+41
View File
@@ -0,0 +1,41 @@
//! Core Endor objects, values, validated executable data and object storage.
//! This crate has no VM evaluator or external dependencies. Handles, reference
//! accounting and mutable payload replacement remain encapsulated here.
//!
//! ```
//! use objects::{ObjectStore, Value};
//! let mut store = ObjectStore::new()?;
//! let object = store.new_object()?;
//! store.set_attr(object, "name", Value::Text("Endor".into()))?;
//! assert_eq!(store.get_attr(object, "name")?, Value::Text("Endor".into()));
//! store.release(object)?;
//! assert_eq!(store.object_count(), 0);
//! # Ok::<(), objects::VmError>(())
//! ```
mod builtins;
mod code;
mod error;
mod heap;
mod instruction;
mod number;
mod object;
mod store;
mod store_builtins;
pub use builtins::{
EndorBaseExceptionObject, EndorCellObject, EndorDictObject, EndorFunctionObject,
EndorStringObject, EndorTracebackEntry,
};
pub use code::{CodeLayout, EndorCode, EndorCodeObject};
pub use error::VmError;
pub use heap::ObjectRef;
pub use instruction::{DecodedInstruction, Instruction, Opcode};
pub use number::{
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
};
pub use object::{
CompareOp, EndorNativeCall, EndorNativeFunctionObject, EndorObject, NativeCall, NativeFunction,
ObjectHeader, ObjectPayload, Record, Value,
};
pub use store::ObjectStore;
+317
View File
@@ -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())
}
+223
View File
@@ -0,0 +1,223 @@
use std::any::Any;
use std::collections::BTreeMap;
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
use crate::{EndorNumber, ObjectRef, VmError};
/// Immediate values stay outside the object heap. Copying an ObjectRef or
/// cloning a Value never retains a reference; ownership follows the API that
/// produced it. In particular, call/execute results own a root until released.
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Nil,
Bool(bool),
/// Rust equality preserves representation; it is not numeric equivalence.
Number(EndorNumber),
Text(String),
Object(ObjectRef),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
impl CompareOp {
pub(crate) fn reflected(self) -> Self {
match self {
Self::Eq => Self::Eq,
Self::Ne => Self::Ne,
Self::Lt => Self::Gt,
Self::Le => Self::Ge,
Self::Gt => Self::Lt,
Self::Ge => Self::Le,
}
}
}
/// Rust-side operation interface, not a language type or metaclass.
///
/// 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 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 {
fn as_any(&self) -> &dyn Any;
/// A diagnostic label only, with no language type or inheritance semantics.
fn kind_name(&self) -> &'static str;
fn trace(&self, _visit: &mut dyn FnMut(ObjectRef)) {}
fn allows_attributes(&self) -> bool {
false
}
fn repr(&self, identity: ObjectRef) -> Result<String, VmError> {
Ok(format!("<{} {identity}>", self.kind_name()))
}
fn str(&self, identity: ObjectRef) -> Result<String, VmError> {
self.repr(identity)
}
fn is_true(&self) -> Result<bool, VmError> {
Ok(true)
}
fn hash(&self, identity: ObjectRef) -> Result<u64, VmError> {
let mut hasher = DefaultHasher::new();
identity.hash(&mut hasher);
Ok(hasher.finish())
}
/// None means "not implemented"; the VM tries the reflected operation,
/// then falls back to identity for Eq/Ne or rejects ordering.
fn compare(
&self,
_other: &dyn ObjectPayload,
_operation: CompareOp,
) -> Result<Option<bool>, VmError> {
Ok(None)
}
/// Host-call entry: arguments and raw results are borrowed handles. The VM
/// validates/retains the result before exposing it as an owned call result.
/// Language functions are dispatched by Vm's frame evaluator instead.
fn call(&self, _arguments: &[Value]) -> Result<Value, VmError> {
Err(VmError::UnsupportedOperation("call"))
}
}
#[derive(Debug, Default)]
pub struct Record;
impl ObjectPayload for Record {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"record"
}
fn allows_attributes(&self) -> bool {
true
}
}
pub type EndorNativeCall = fn(&[Value]) -> Result<Value, VmError>;
pub type NativeCall = EndorNativeCall;
#[derive(Debug)]
pub struct EndorNativeFunctionObject {
name: String,
function: NativeCall,
}
impl EndorNativeFunctionObject {
pub fn new(name: impl Into<String>, function: NativeCall) -> Self {
Self {
name: name.into(),
function,
}
}
pub fn name(&self) -> &str {
&self.name
}
}
/// Compatibility name for existing embedders.
pub type NativeFunction = EndorNativeFunctionObject;
impl ObjectPayload for EndorNativeFunctionObject {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"native-function"
}
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
Ok(format!("<native-function {}>", self.name))
}
fn call(&self, arguments: &[Value]) -> Result<Value, VmError> {
(self.function)(arguments)
}
}
/// Common runtime state. There is deliberately no language type pointer.
#[derive(Debug)]
pub struct ObjectHeader {
pub(crate) identity: ObjectRef,
pub(crate) strong_refs: usize,
pub(crate) external_refs: usize,
}
impl ObjectHeader {
pub fn identity(&self) -> ObjectRef {
self.identity
}
pub fn ref_count(&self) -> usize {
self.strong_refs
}
/// Host-owned references and live execution roots, excluding heap edges.
pub fn external_ref_count(&self) -> usize {
self.external_refs
}
}
/// Runtime object foundation, analogous in role to CPython's common object
/// structure, without its type system or binary layout. The heap boxes this
/// structure so its address remains stable throughout its lifetime.
#[derive(Debug)]
pub struct EndorObject {
pub(crate) header: ObjectHeader,
pub(crate) payload: Box<dyn ObjectPayload>,
pub(crate) attributes: BTreeMap<String, Value>,
pub(crate) payload_edges: Vec<ObjectRef>,
}
impl EndorObject {
pub fn header(&self) -> &ObjectHeader {
&self.header
}
pub fn payload(&self) -> &dyn ObjectPayload {
self.payload.as_ref()
}
/// Visits owned edges, not external roots. Payload edges are snapshotted at
/// 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);
}
for value in self.attributes.values() {
if let Value::Object(reference) = value {
visit(*reference);
}
}
}
pub(crate) fn edges(&self) -> Vec<ObjectRef> {
let mut edges = Vec::new();
self.trace(&mut |reference| edges.push(reference));
edges
}
}
+125
View File
@@ -0,0 +1,125 @@
use crate::heap::Heap;
use crate::{CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value, VmError};
/// Object storage independent of the VM evaluator. Construction creates an empty
/// heap; dropping it releases all objects and their host resources. Mutable
/// access is limited to operations that validate and account for owned edges.
#[derive(Debug)]
pub struct ObjectStore {
pub(crate) heap: Heap,
}
impl ObjectStore {
pub fn new() -> Result<Self, VmError> {
Ok(Self { heap: Heap::new()? })
}
pub fn object_count(&self) -> usize {
self.heap.len()
}
/// 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))
}
pub fn new_object(&mut self) -> Result<ObjectRef, VmError> {
self.allocate(Record)
}
pub fn object(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> {
self.heap.get(reference)
}
pub fn retain(&mut self, reference: ObjectRef) -> Result<(), VmError> {
self.heap.retain(reference)
}
pub fn release(&mut self, reference: ObjectRef) -> Result<(), VmError> {
self.heap.release(reference)
}
pub fn ref_count(&self, reference: ObjectRef) -> Result<usize, VmError> {
Ok(self.heap.get(reference)?.header().ref_count())
}
pub fn is_same(&self, left: ObjectRef, right: ObjectRef) -> Result<bool, VmError> {
self.heap.get(left)?;
self.heap.get(right)?;
Ok(left == right)
}
pub fn repr(&self, reference: ObjectRef) -> Result<String, VmError> {
self.heap.get(reference)?.payload().repr(reference)
}
pub fn str(&self, reference: ObjectRef) -> Result<String, VmError> {
self.heap.get(reference)?.payload().str(reference)
}
pub fn is_true(&self, reference: ObjectRef) -> Result<bool, VmError> {
self.heap.get(reference)?.payload().is_true()
}
pub fn hash(&self, reference: ObjectRef) -> Result<u64, VmError> {
self.heap.get(reference)?.payload().hash(reference)
}
pub fn compare(
&self,
left: ObjectRef,
right: ObjectRef,
operation: CompareOp,
) -> Result<bool, VmError> {
let left_payload = self.heap.get(left)?.payload();
let right_payload = self.heap.get(right)?.payload();
if let Some(result) = left_payload.compare(right_payload, operation)? {
return Ok(result);
}
if let Some(result) = right_payload.compare(left_payload, operation.reflected())? {
return Ok(result);
}
match operation {
CompareOp::Eq => Ok(left == right),
CompareOp::Ne => Ok(left != right),
_ => Err(VmError::UnsupportedOperation("ordering")),
}
}
/// Reads a borrowed value. An Object result needs retain() if the host
/// wants it to survive removal of the attribute that currently owns it.
pub fn get_attr(&self, reference: ObjectRef, name: &str) -> Result<Value, VmError> {
self.heap.get_attr(reference, name)
}
/// Object-valued attributes own internal strong references independently
/// of the caller's external references. Failure leaves the field unchanged.
pub fn set_attr(
&mut self,
reference: ObjectRef,
name: impl Into<String>,
value: Value,
) -> Result<(), VmError> {
self.heap.set_attr(reference, name.into(), value)
}
pub fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> {
self.heap.del_attr(reference, name)
}
/// Validate a borrowed object handle without taking an owned reference.
pub fn validate_value(&self, value: &Value) -> Result<(), VmError> {
if let Value::Object(reference) = value {
self.heap.get(*reference)?;
}
Ok(())
}
/// Explicit full collection. Execution slots use owned roots, so values
/// on live execution stacks participate in the same root accounting.
pub fn collect_cycles(&mut self) -> usize {
self.heap.collect_cycles()
}
}
+215
View File
@@ -0,0 +1,215 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use crate::{
CompareOp, EndorBaseExceptionObject, EndorCellObject, EndorCode, EndorCodeObject,
EndorDictObject, EndorFunctionObject, EndorNumber, EndorNumberObject, EndorStringObject,
ObjectPayload, ObjectRef, ObjectStore, Value, VmError,
};
impl ObjectStore {
pub(crate) fn validate_builtin(&self, payload: &dyn ObjectPayload) -> Result<(), VmError> {
if let Some(function) = payload.as_any().downcast_ref::<EndorFunctionObject>() {
let code = self
.payload::<EndorCodeObject>(function.code(), "code")?
.code();
if function.defaults().len() > code.parameters().len()
|| function.closure().len() != code.free_variables().len()
{
return Err(VmError::InvalidArgument(
"function defaults or closure do not match code",
));
}
self.payload::<EndorDictObject>(function.globals(), "dict")?;
if let Some(builtins) = function.builtins() {
self.payload::<EndorDictObject>(builtins, "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_code(&mut self, code: EndorCode) -> Result<ObjectRef, VmError> {
self.allocate(EndorCodeObject::new(code))
}
pub fn new_function(
&mut self,
name: impl Into<String>,
code: ObjectRef,
globals: ObjectRef,
defaults: Vec<Value>,
closure: Vec<ObjectRef>,
) -> Result<ObjectRef, VmError> {
self.payload::<EndorDictObject>(globals, "dict")?;
let builtins = match self.dict_get(globals, &Value::Text("__builtins__".into()))? {
Some(Value::Object(reference)) => {
self.payload::<EndorDictObject>(reference, "dict")?;
Some(reference)
}
None => None,
_ => return Err(VmError::ObjectKindMismatch("dict")),
};
self.allocate(EndorFunctionObject::new(
name.into(),
code,
globals,
builtins,
defaults,
closure,
))
}
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())
}
}