refactor: extract core objects into a standalone package
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s
This commit is contained in:
@@ -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)。
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user