feat: add managed types, source classes and scoped native calls
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
| --- | --- |
|
||||
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出与独立存储 rustdoc 示例 |
|
||||
| [object.rs](object.rs) | Value、CompareOp、ObjectPayload、ObjectHeader、EndorObject、Record、NativeFunction |
|
||||
| [types.rs](types.rs) | 托管名义类型、显式 Object/Type 引导、继承查询、受管方法表/接收者绑定、类字段与原生构造委托 |
|
||||
| [native.rs](native.rs) | NativeContext、上下文原生函数、作用域根与非递归委托调用 |
|
||||
| [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 |
|
||||
| [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、引用事务、迭代释放、循环回收及私有边界测试 |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::ObjectRef;
|
||||
|
||||
/// A precise field change or a replacement of an object's native contents.
|
||||
/// Entries are notifications, never roots; a source can be dead when consumed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChangeKey {
|
||||
Attribute(String),
|
||||
Contents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ObjectChange {
|
||||
pub object: ObjectRef,
|
||||
pub key: ChangeKey,
|
||||
pub version: u64,
|
||||
}
|
||||
|
||||
/// Coalesced changes since the previous drain. Overflow requires the consumer
|
||||
/// to rescan its dependencies, rather than silently missing notifications.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ChangeBatch {
|
||||
pub changes: Vec<ObjectChange>,
|
||||
pub rescan_required: bool,
|
||||
}
|
||||
|
||||
pub(crate) const MAX_PENDING_CHANGES: usize = 4096;
|
||||
@@ -344,6 +344,7 @@ fn verify(
|
||||
let consumed = defaults + closure + 1;
|
||||
(consumed, consumed, 1)
|
||||
}
|
||||
Opcode::BuildClass => (4, 4, 1),
|
||||
Opcode::ReturnValue => (1, 1, 0),
|
||||
_ => (0, 0, 0),
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crate::changes::MAX_PENDING_CHANGES;
|
||||
use crate::{ChangeBatch, ChangeKey, ObjectChange};
|
||||
use crate::{EndorObject, ObjectHeader, ObjectPayload, Value, VmError};
|
||||
|
||||
static NEXT_HEAP_ID: AtomicU64 = AtomicU64::new(1);
|
||||
@@ -21,6 +23,11 @@ impl fmt::Display for ObjectRef {
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-owning reference. Only the originating store can upgrade it; slot
|
||||
/// reuse never makes a dead weak reference point at a different object.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct WeakObjectRef(pub(crate) ObjectRef);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Slot {
|
||||
generation: u64,
|
||||
@@ -33,6 +40,8 @@ pub(crate) struct Heap {
|
||||
slots: Vec<Slot>,
|
||||
free: Vec<usize>,
|
||||
live: usize,
|
||||
changes: BTreeMap<(ObjectRef, ChangeKey), u64>,
|
||||
changes_overflow: bool,
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
@@ -45,6 +54,8 @@ impl Heap {
|
||||
slots: Vec::new(),
|
||||
free: Vec::new(),
|
||||
live: 0,
|
||||
changes: BTreeMap::new(),
|
||||
changes_overflow: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,6 +83,8 @@ impl Heap {
|
||||
identity: reference,
|
||||
strong_refs: 1,
|
||||
external_refs: 1,
|
||||
change_version: 0,
|
||||
type_object: None,
|
||||
},
|
||||
payload,
|
||||
attributes: BTreeMap::new(),
|
||||
@@ -92,6 +105,27 @@ impl Heap {
|
||||
.ok_or(VmError::InvalidHandle)
|
||||
}
|
||||
|
||||
pub(crate) fn attach_type(
|
||||
&mut self,
|
||||
reference: ObjectRef,
|
||||
type_object: ObjectRef,
|
||||
) -> Result<(), VmError> {
|
||||
if self.get(reference)?.header.type_object.is_some() {
|
||||
return Err(VmError::InvalidArgument("object already has a type"));
|
||||
}
|
||||
if self.get(reference)?.attributes.contains_key("type") {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"type attribute conflicts with managed type",
|
||||
));
|
||||
}
|
||||
let increments = self.prepare_increments(&[type_object])?;
|
||||
self.prepare_change(reference)?;
|
||||
self.apply_increments(increments);
|
||||
self.get_mut(reference)?.header.type_object = Some(type_object);
|
||||
self.record_change(reference, ChangeKey::Contents);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -104,10 +138,12 @@ impl Heap {
|
||||
let mut edges = Vec::new();
|
||||
payload.trace(&mut |target| edges.push(target));
|
||||
let increments = self.prepare_increments(&edges)?;
|
||||
self.prepare_change(reference)?;
|
||||
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.record_change(reference, ChangeKey::Contents);
|
||||
self.release_edges(old_edges);
|
||||
drop(old_payload);
|
||||
Ok(())
|
||||
@@ -194,8 +230,13 @@ impl Heap {
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let increments = self.prepare_increments(&edges)?;
|
||||
self.prepare_change(reference)?;
|
||||
self.apply_increments(increments);
|
||||
let old = self.get_mut(reference)?.attributes.insert(name, value);
|
||||
let old = self
|
||||
.get_mut(reference)?
|
||||
.attributes
|
||||
.insert(name.clone(), value);
|
||||
self.record_change(reference, ChangeKey::Attribute(name));
|
||||
if let Some(Value::Object(target)) = old {
|
||||
self.release_edges(vec![target]);
|
||||
}
|
||||
@@ -213,17 +254,58 @@ impl Heap {
|
||||
|
||||
pub(crate) fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> {
|
||||
self.check_attributes(reference)?;
|
||||
self.prepare_change(reference)?;
|
||||
let old = self
|
||||
.get_mut(reference)?
|
||||
.attributes
|
||||
.remove(name)
|
||||
.ok_or_else(|| VmError::AttributeNotFound(name.into()))?;
|
||||
self.record_change(reference, ChangeKey::Attribute(name.into()));
|
||||
if let Value::Object(target) = old {
|
||||
self.release_edges(vec![target]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_change(&self, reference: ObjectRef) -> Result<(), VmError> {
|
||||
self.get(reference)?
|
||||
.header
|
||||
.change_version
|
||||
.checked_add(1)
|
||||
.ok_or(VmError::ExecutionLimit("object change version"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn record_change(&mut self, reference: ObjectRef, key: ChangeKey) {
|
||||
let object = self.get_mut(reference).expect("validated change target");
|
||||
object.header.change_version += 1;
|
||||
let version = object.header.change_version;
|
||||
if self.changes_overflow {
|
||||
return;
|
||||
}
|
||||
let entry = (reference, key);
|
||||
if self.changes.len() >= MAX_PENDING_CHANGES && !self.changes.contains_key(&entry) {
|
||||
self.changes.clear();
|
||||
self.changes_overflow = true;
|
||||
} else {
|
||||
self.changes.insert(entry, version);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_changes(&mut self) -> ChangeBatch {
|
||||
ChangeBatch {
|
||||
changes: std::mem::take(&mut self.changes)
|
||||
.into_iter()
|
||||
.map(|((object, key), version)| ObjectChange {
|
||||
object,
|
||||
key,
|
||||
version,
|
||||
})
|
||||
.collect(),
|
||||
rescan_required: std::mem::take(&mut self.changes_overflow),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_attributes(&self, reference: ObjectRef) -> Result<(), VmError> {
|
||||
if !self.get(reference)?.payload.allows_attributes() {
|
||||
return Err(VmError::UnsupportedOperation("attributes"));
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum Opcode {
|
||||
StoreAttr = 26,
|
||||
BuildMap = 27,
|
||||
MakeFunction = 28,
|
||||
BuildClass = 29,
|
||||
ExtendedArg = 255,
|
||||
}
|
||||
|
||||
@@ -69,6 +70,7 @@ impl Opcode {
|
||||
26 => Self::StoreAttr,
|
||||
27 => Self::BuildMap,
|
||||
28 => Self::MakeFunction,
|
||||
29 => Self::BuildClass,
|
||||
255 => Self::ExtendedArg,
|
||||
_ => return None,
|
||||
})
|
||||
@@ -83,6 +85,7 @@ impl Opcode {
|
||||
| Self::ReturnValue
|
||||
| Self::ToBool
|
||||
| Self::UnaryNot
|
||||
| Self::BuildClass
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,23 +14,28 @@
|
||||
//! ```
|
||||
|
||||
mod builtins;
|
||||
mod changes;
|
||||
mod code;
|
||||
mod error;
|
||||
mod heap;
|
||||
mod instruction;
|
||||
mod native;
|
||||
mod number;
|
||||
mod object;
|
||||
mod store;
|
||||
mod store_builtins;
|
||||
mod types;
|
||||
|
||||
pub use builtins::{
|
||||
EndorBaseExceptionObject, EndorCellObject, EndorDictObject, EndorFunctionObject,
|
||||
EndorStringObject, EndorTracebackEntry,
|
||||
};
|
||||
pub use changes::{ChangeBatch, ChangeKey, ObjectChange};
|
||||
pub use code::{CodeLayout, EndorCode, EndorCodeObject};
|
||||
pub use error::VmError;
|
||||
pub use heap::ObjectRef;
|
||||
pub use heap::{ObjectRef, WeakObjectRef};
|
||||
pub use instruction::{DecodedInstruction, Instruction, Opcode};
|
||||
pub use native::{ContextNativeFunction, NativeCallable, NativeContext, NativeResult};
|
||||
pub use number::{
|
||||
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
|
||||
};
|
||||
@@ -39,3 +44,4 @@ pub use object::{
|
||||
ObjectHeader, ObjectPayload, Record, Value,
|
||||
};
|
||||
pub use store::ObjectStore;
|
||||
pub use types::{ClassField, EndorTypeObject, RootTypes, TypeConstructor};
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use crate::{ObjectPayload, ObjectRef, ObjectStore, Value, VmError, WeakObjectRef};
|
||||
|
||||
/// Borrowed results produced inside a native call. Invoke delegates to another
|
||||
/// callable through the VM's existing frame loop, without recursively entering it.
|
||||
#[derive(Debug)]
|
||||
pub enum NativeResult {
|
||||
Return(Value),
|
||||
Invoke {
|
||||
function: ObjectRef,
|
||||
arguments: Vec<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait NativeCallable: fmt::Debug + Send + Sync + 'static {
|
||||
fn invoke(
|
||||
&self,
|
||||
context: &mut NativeContext<'_>,
|
||||
arguments: &[Value],
|
||||
) -> Result<NativeResult, VmError>;
|
||||
}
|
||||
|
||||
/// Native allocation/mutation scope. Allocations and upgraded weak references
|
||||
/// are temporary owned roots. Return/Invoke promotes only its output roots;
|
||||
/// Drop releases everything else, including on error or unwinding.
|
||||
pub struct NativeContext<'a> {
|
||||
store: &'a mut ObjectStore,
|
||||
roots: Vec<ObjectRef>,
|
||||
callee: ObjectRef,
|
||||
}
|
||||
|
||||
impl<'a> NativeContext<'a> {
|
||||
/// Callable currently dispatched by the VM, borrowed for this scope.
|
||||
pub fn callee(&self) -> ObjectRef {
|
||||
self.callee
|
||||
}
|
||||
|
||||
pub fn attach_type(
|
||||
&mut self,
|
||||
object: ObjectRef,
|
||||
type_object: ObjectRef,
|
||||
) -> Result<(), VmError> {
|
||||
self.store.attach_type(object, type_object)
|
||||
}
|
||||
|
||||
pub fn store(&self) -> &ObjectStore {
|
||||
self.store
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, payload: impl ObjectPayload) -> Result<ObjectRef, VmError> {
|
||||
let object = self.store.allocate(payload)?;
|
||||
self.roots.push(object);
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
pub fn new_object(&mut self) -> Result<ObjectRef, VmError> {
|
||||
self.allocate(crate::Record)
|
||||
}
|
||||
|
||||
pub fn new_dict(&mut self) -> Result<ObjectRef, VmError> {
|
||||
let object = self.store.new_dict()?;
|
||||
self.roots.push(object);
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
pub fn new_cell(&mut self, value: Option<Value>) -> Result<ObjectRef, VmError> {
|
||||
let object = self.store.new_cell(value)?;
|
||||
self.roots.push(object);
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
pub fn set_attr(
|
||||
&mut self,
|
||||
object: ObjectRef,
|
||||
name: impl Into<String>,
|
||||
value: Value,
|
||||
) -> Result<(), VmError> {
|
||||
self.store.set_attr(object, name, value)
|
||||
}
|
||||
|
||||
pub fn del_attr(&mut self, object: ObjectRef, name: &str) -> Result<(), VmError> {
|
||||
self.store.del_attr(object, name)
|
||||
}
|
||||
|
||||
pub fn dict_set(&mut self, object: ObjectRef, key: Value, value: Value) -> Result<(), VmError> {
|
||||
self.store.dict_set(object, key, value)
|
||||
}
|
||||
|
||||
pub fn dict_delete(&mut self, object: ObjectRef, key: &Value) -> Result<bool, VmError> {
|
||||
self.store.dict_delete(object, key)
|
||||
}
|
||||
|
||||
pub fn cell_set(&mut self, object: ObjectRef, value: Value) -> Result<(), VmError> {
|
||||
self.store.cell_set(object, value)
|
||||
}
|
||||
|
||||
pub fn replace_payload(
|
||||
&mut self,
|
||||
object: ObjectRef,
|
||||
payload: impl ObjectPayload,
|
||||
) -> Result<(), VmError> {
|
||||
self.store.replace_payload(object, payload)
|
||||
}
|
||||
|
||||
pub fn upgrade(&mut self, weak: WeakObjectRef) -> Result<Option<ObjectRef>, VmError> {
|
||||
let object = self.store.upgrade(weak)?;
|
||||
if let Some(object) = object {
|
||||
self.roots.push(object);
|
||||
}
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
fn protect(&mut self, value: &Value) -> Result<(), VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
self.store.retain(*object)?;
|
||||
self.roots.push(*object);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal execution bridge. Every returned object reference owns a root;
|
||||
/// consumers must release Return values or all Invoke arguments/function.
|
||||
pub fn execute(
|
||||
store: &'a mut ObjectStore,
|
||||
function: ObjectRef,
|
||||
arguments: &[Value],
|
||||
callable: Arc<dyn NativeCallable>,
|
||||
) -> Result<NativeResult, VmError> {
|
||||
let mut context = Self {
|
||||
store,
|
||||
roots: Vec::new(),
|
||||
callee: function,
|
||||
};
|
||||
context.protect(&Value::Object(function))?;
|
||||
for argument in arguments {
|
||||
context.protect(argument)?;
|
||||
}
|
||||
let result = callable.invoke(&mut context, arguments)?;
|
||||
let transferred_from = context.roots.len();
|
||||
match &result {
|
||||
NativeResult::Return(value) => context.protect(value)?,
|
||||
NativeResult::Invoke {
|
||||
function,
|
||||
arguments,
|
||||
} => {
|
||||
context.protect(&Value::Object(*function))?;
|
||||
for argument in arguments {
|
||||
context.protect(argument)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transfer exactly the output roots; all other roots remain scoped.
|
||||
context.roots.truncate(transferred_from);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeContext<'_> {
|
||||
fn drop(&mut self) {
|
||||
for root in self.roots.drain(..).rev() {
|
||||
self.store
|
||||
.release(root)
|
||||
.expect("native scope owns its roots");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A context-aware function with explicit, immutable captured object edges.
|
||||
#[derive(Debug)]
|
||||
pub struct ContextNativeFunction {
|
||||
name: &'static str,
|
||||
callable: Arc<dyn NativeCallable>,
|
||||
captures: Vec<ObjectRef>,
|
||||
}
|
||||
|
||||
impl ContextNativeFunction {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
callable: impl NativeCallable,
|
||||
captures: Vec<ObjectRef>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
callable: Arc::new(callable),
|
||||
captures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectPayload for ContextNativeFunction {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn kind_name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
for capture in &self.captures {
|
||||
visit(*capture);
|
||||
}
|
||||
}
|
||||
fn native_callable(&self) -> Option<Arc<dyn NativeCallable>> {
|
||||
Some(self.callable.clone())
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,19 @@ impl CompareOp {
|
||||
/// 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 {
|
||||
/// Shared-reference facades may expose the current referent's type. The
|
||||
/// referent must also be held by traced managed edges. Ordinary objects
|
||||
/// keep their immutable header type; this does not change inheritance.
|
||||
fn type_target(&self, _store: &crate::ObjectStore) -> Result<Option<ObjectRef>, VmError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Optional context-aware native entry. The VM clones this dispatcher before
|
||||
/// borrowing the store mutably; callbacks never borrow a live payload.
|
||||
fn native_callable(&self) -> Option<std::sync::Arc<dyn crate::NativeCallable>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// A diagnostic label only, with no language type or inheritance semantics.
|
||||
@@ -104,6 +117,18 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync {
|
||||
fn assign_from(&self, _source: &dyn ObjectPayload) -> Result<bool, VmError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Optional managed shared-reference assignment. Return a built-in Cell
|
||||
/// owned by this payload after validating the source and any domain cycles.
|
||||
/// The store writes the source object to the Cell transactionally. This
|
||||
/// keeps mutable edges inside the GC graph, unlike native assign_from.
|
||||
fn assignment_cell(
|
||||
&self,
|
||||
_source: ObjectRef,
|
||||
_store: &crate::ObjectStore,
|
||||
) -> Result<Option<ObjectRef>, VmError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -166,15 +191,25 @@ impl ObjectPayload for EndorNativeFunctionObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Common runtime state. There is deliberately no language type pointer.
|
||||
/// Common runtime state. Embedders may attach an immutable managed type.
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectHeader {
|
||||
pub(crate) identity: ObjectRef,
|
||||
pub(crate) strong_refs: usize,
|
||||
pub(crate) external_refs: usize,
|
||||
pub(crate) change_version: u64,
|
||||
pub(crate) type_object: Option<ObjectRef>,
|
||||
}
|
||||
|
||||
impl ObjectHeader {
|
||||
pub fn type_object(&self) -> Option<ObjectRef> {
|
||||
self.type_object
|
||||
}
|
||||
|
||||
pub fn change_version(&self) -> u64 {
|
||||
self.change_version
|
||||
}
|
||||
|
||||
pub fn identity(&self) -> ObjectRef {
|
||||
self.identity
|
||||
}
|
||||
@@ -190,7 +225,7 @@ impl ObjectHeader {
|
||||
}
|
||||
|
||||
/// Runtime object foundation, analogous in role to CPython's common object
|
||||
/// structure, without its type system or binary layout. The heap boxes this
|
||||
/// structure, without its binary layout. The heap boxes this
|
||||
/// structure so its address remains stable throughout its lifetime.
|
||||
#[derive(Debug)]
|
||||
pub struct EndorObject {
|
||||
@@ -213,6 +248,9 @@ impl EndorObject {
|
||||
/// 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)) {
|
||||
if let Some(reference) = self.header.type_object {
|
||||
visit(reference);
|
||||
}
|
||||
for reference in &self.payload_edges {
|
||||
visit(*reference);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::heap::Heap;
|
||||
use crate::{CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value, VmError};
|
||||
use crate::{
|
||||
ChangeBatch, ChangeKey, CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value,
|
||||
VmError, WeakObjectRef,
|
||||
};
|
||||
|
||||
/// Object storage independent of the VM evaluator. Construction creates an empty
|
||||
/// heap; dropping it releases all objects and their host resources. Mutable
|
||||
@@ -10,6 +13,80 @@ pub struct ObjectStore {
|
||||
}
|
||||
|
||||
impl ObjectStore {
|
||||
pub fn downgrade(&self, reference: ObjectRef) -> Result<WeakObjectRef, VmError> {
|
||||
self.object(reference)?;
|
||||
Ok(WeakObjectRef(reference))
|
||||
}
|
||||
|
||||
/// Returns a new owned root, or None when the original object was released.
|
||||
/// A foreign store is an error, not an expired reference.
|
||||
pub fn upgrade(&mut self, weak: WeakObjectRef) -> Result<Option<ObjectRef>, VmError> {
|
||||
match self.object(weak.0) {
|
||||
Ok(_) => {
|
||||
self.retain(weak.0)?;
|
||||
Ok(Some(weak.0))
|
||||
}
|
||||
Err(VmError::InvalidHandle) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_changes(&mut self) -> ChangeBatch {
|
||||
self.heap.take_changes()
|
||||
}
|
||||
|
||||
pub fn change_version(&self, reference: ObjectRef) -> Result<u64, VmError> {
|
||||
Ok(self.object(reference)?.header().change_version())
|
||||
}
|
||||
|
||||
/// Replace a host payload transactionally, including all owned GC edges.
|
||||
pub fn replace_payload(
|
||||
&mut self,
|
||||
reference: ObjectRef,
|
||||
payload: impl ObjectPayload,
|
||||
) -> Result<(), VmError> {
|
||||
if self
|
||||
.object(reference)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.is::<crate::EndorTypeObject>()
|
||||
{
|
||||
return Err(VmError::UnsupportedOperation(
|
||||
"replace immutable type definition",
|
||||
));
|
||||
}
|
||||
self.validate_builtin(&payload)?;
|
||||
self.heap.replace_payload(reference, payload)
|
||||
}
|
||||
|
||||
pub fn assign_from(&mut self, target: ObjectRef, source: ObjectRef) -> Result<bool, VmError> {
|
||||
self.heap.prepare_change(target)?;
|
||||
self.object(source)?;
|
||||
if let Some(cell) = self
|
||||
.object(target)?
|
||||
.payload()
|
||||
.assignment_cell(source, self)?
|
||||
{
|
||||
// Replacing the Cell edge may otherwise reclaim a target which is
|
||||
// reachable only through the edge being removed.
|
||||
self.retain(target)?;
|
||||
let result = self.cell_set(cell, Value::Object(source));
|
||||
if result.is_ok() {
|
||||
self.heap.record_change(target, ChangeKey::Contents);
|
||||
}
|
||||
self.release(target)?;
|
||||
return result.map(|()| true);
|
||||
}
|
||||
let changed = self
|
||||
.object(target)?
|
||||
.payload()
|
||||
.assign_from(self.object(source)?.payload())?;
|
||||
if changed {
|
||||
self.heap.record_change(target, ChangeKey::Contents);
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, VmError> {
|
||||
Ok(Self { heap: Heap::new()? })
|
||||
}
|
||||
@@ -91,7 +168,11 @@ impl ObjectStore {
|
||||
/// 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)
|
||||
self.lookup_attr(reference, name)
|
||||
.map(|attribute| match attribute {
|
||||
crate::types::AttributeLookup::Value(value) => value,
|
||||
crate::types::AttributeLookup::Method { function, .. } => Value::Object(function),
|
||||
})
|
||||
}
|
||||
|
||||
/// Object-valued attributes own internal strong references independently
|
||||
@@ -102,10 +183,17 @@ impl ObjectStore {
|
||||
name: impl Into<String>,
|
||||
value: Value,
|
||||
) -> Result<(), VmError> {
|
||||
self.heap.set_attr(reference, name.into(), value)
|
||||
let name = name.into();
|
||||
if name == "type" && self.type_of(reference)?.is_some() {
|
||||
return Err(VmError::UnsupportedOperation("write object type"));
|
||||
}
|
||||
self.heap.set_attr(reference, name, value)
|
||||
}
|
||||
|
||||
pub fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> {
|
||||
if name == "type" && self.type_of(reference)?.is_some() {
|
||||
return Err(VmError::UnsupportedOperation("delete object type"));
|
||||
}
|
||||
self.heap.del_attr(reference, name)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
//! Nominal type identities in the same managed heap as their instances.
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, ObjectStore, Value,
|
||||
VmError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TypeConstructor {
|
||||
/// Allocate a plain, attribute-bearing instance of this type.
|
||||
Record,
|
||||
/// Delegate to a managed callable. It must return an instance of this type.
|
||||
Native(ObjectRef),
|
||||
Abstract,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ClassField {
|
||||
pub name: String,
|
||||
/// Preserved source annotation, like current function annotations.
|
||||
pub type_name: String,
|
||||
}
|
||||
|
||||
/// Nominal definition with immutable identity/base and a managed method table.
|
||||
/// Its base, constructor and metatype are traced
|
||||
/// edges; no registry outside the heap keeps unused definitions alive.
|
||||
#[derive(Debug)]
|
||||
pub struct EndorTypeObject {
|
||||
name: String,
|
||||
base: Option<ObjectRef>,
|
||||
constructor: TypeConstructor,
|
||||
methods: BTreeMap<String, ObjectRef>,
|
||||
fields: Vec<ClassField>,
|
||||
}
|
||||
|
||||
impl EndorTypeObject {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn base(&self) -> Option<ObjectRef> {
|
||||
self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectPayload for EndorTypeObject {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"type"
|
||||
}
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
if let Some(base) = self.base {
|
||||
visit(base);
|
||||
}
|
||||
if let TypeConstructor::Native(function) = self.constructor {
|
||||
visit(function);
|
||||
}
|
||||
for method in self.methods.values() {
|
||||
visit(*method);
|
||||
}
|
||||
}
|
||||
fn repr(&self, _: ObjectRef) -> Result<String, VmError> {
|
||||
Ok(format!("<type {}>", self.name))
|
||||
}
|
||||
fn native_callable(&self) -> Option<Arc<dyn NativeCallable>> {
|
||||
Some(Arc::new(Construct))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Construct;
|
||||
impl NativeCallable for Construct {
|
||||
fn invoke(
|
||||
&self,
|
||||
cx: &mut NativeContext<'_>,
|
||||
arguments: &[Value],
|
||||
) -> Result<NativeResult, VmError> {
|
||||
let type_object = cx.callee();
|
||||
let definition = cx.store().payload::<EndorTypeObject>(type_object, "Type")?;
|
||||
match definition.constructor {
|
||||
TypeConstructor::Native(function) => Ok(NativeResult::Invoke {
|
||||
function,
|
||||
arguments: arguments.to_vec(),
|
||||
}),
|
||||
TypeConstructor::Abstract => {
|
||||
Err(VmError::UnsupportedOperation("abstract type construction"))
|
||||
}
|
||||
TypeConstructor::Record => {
|
||||
if arguments.len() != definition.fields.len() {
|
||||
return Err(VmError::ArgumentCount {
|
||||
name: definition.name.clone(),
|
||||
required: definition.fields.len(),
|
||||
maximum: definition.fields.len(),
|
||||
provided: arguments.len(),
|
||||
});
|
||||
}
|
||||
let fields = definition.fields.clone();
|
||||
let instance = cx.new_object()?;
|
||||
cx.attach_type(instance, type_object)?;
|
||||
for (field, value) in fields.iter().zip(arguments) {
|
||||
cx.set_attr(instance, &field.name, value.clone())?;
|
||||
}
|
||||
Ok(NativeResult::Return(Value::Object(instance)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Each field owns one external root; the caller must release both.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RootTypes {
|
||||
pub object: ObjectRef,
|
||||
pub type_type: ObjectRef,
|
||||
}
|
||||
|
||||
impl ObjectStore {
|
||||
/// Explicit bootstrap preserves the empty-store contract. Inheritance:
|
||||
/// Type -> Object; instance relations: Object : Type and Type : Type.
|
||||
pub fn bootstrap_types(&mut self) -> Result<RootTypes, VmError> {
|
||||
let object = self.allocate(EndorTypeObject {
|
||||
name: "Object".into(),
|
||||
base: None,
|
||||
constructor: TypeConstructor::Record,
|
||||
methods: BTreeMap::new(),
|
||||
fields: Vec::new(),
|
||||
})?;
|
||||
let mut allocated_type = None;
|
||||
let result = (|| {
|
||||
let type_type = self.allocate(EndorTypeObject {
|
||||
name: "Type".into(),
|
||||
base: Some(object),
|
||||
constructor: TypeConstructor::Abstract,
|
||||
methods: BTreeMap::new(),
|
||||
fields: Vec::new(),
|
||||
})?;
|
||||
allocated_type = Some(type_type);
|
||||
self.attach_type(object, type_type)?;
|
||||
self.attach_type(type_type, type_type)?;
|
||||
Ok(RootTypes { object, type_type })
|
||||
})();
|
||||
if result.is_err() {
|
||||
if let Some(type_type) = allocated_type {
|
||||
self.release(type_type)?;
|
||||
}
|
||||
self.release(object)?;
|
||||
self.collect_cycles();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Base links are fixed at allocation, preventing inheritance cycles.
|
||||
pub fn new_type(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
base: ObjectRef,
|
||||
constructor: TypeConstructor,
|
||||
) -> Result<ObjectRef, VmError> {
|
||||
self.payload::<EndorTypeObject>(base, "Type")?;
|
||||
let metatype = self
|
||||
.type_of(base)?
|
||||
.ok_or(VmError::InvalidArgument("base type has no metatype"))?;
|
||||
let name = name.into();
|
||||
if name.is_empty() {
|
||||
return Err(VmError::InvalidArgument("type name must not be empty"));
|
||||
}
|
||||
let definition = self.allocate(EndorTypeObject {
|
||||
name,
|
||||
base: Some(base),
|
||||
constructor,
|
||||
methods: BTreeMap::new(),
|
||||
fields: Vec::new(),
|
||||
})?;
|
||||
if let Err(error) = self.attach_type(definition, metatype) {
|
||||
self.release(definition)?;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(definition)
|
||||
}
|
||||
|
||||
pub fn attach_type(
|
||||
&mut self,
|
||||
object: ObjectRef,
|
||||
type_object: ObjectRef,
|
||||
) -> Result<(), VmError> {
|
||||
self.payload::<EndorTypeObject>(type_object, "Type")?;
|
||||
self.heap.attach_type(object, type_object)
|
||||
}
|
||||
|
||||
/// Borrowed type reference; legacy/untyped host objects return None.
|
||||
pub fn type_of(&self, object: ObjectRef) -> Result<Option<ObjectRef>, VmError> {
|
||||
let mut current = object;
|
||||
let mut visited = std::collections::BTreeSet::new();
|
||||
loop {
|
||||
if !visited.insert(current) {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"cyclic shared-reference type target",
|
||||
));
|
||||
}
|
||||
let instance = self.object(current)?;
|
||||
match instance.payload().type_target(self)? {
|
||||
Some(target) => current = target,
|
||||
None => return Ok(instance.header().type_object()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_subtype(&self, candidate: ObjectRef, expected: ObjectRef) -> Result<bool, VmError> {
|
||||
self.payload::<EndorTypeObject>(expected, "Type")?;
|
||||
let mut current = Some(candidate);
|
||||
while let Some(type_object) = current {
|
||||
let definition = self.payload::<EndorTypeObject>(type_object, "Type")?;
|
||||
if type_object == expected {
|
||||
return Ok(true);
|
||||
}
|
||||
current = definition.base;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn is_instance(&self, object: ObjectRef, expected: ObjectRef) -> Result<bool, VmError> {
|
||||
self.payload::<EndorTypeObject>(expected, "Type")?;
|
||||
match self.type_of(object)? {
|
||||
Some(type_object) => self.is_subtype(type_object, expected),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updating the managed method table preserves nominal identity and base.
|
||||
pub fn define_method(
|
||||
&mut self,
|
||||
type_object: ObjectRef,
|
||||
name: impl Into<String>,
|
||||
function: ObjectRef,
|
||||
) -> Result<(), VmError> {
|
||||
self.object(function)?;
|
||||
let name = name.into();
|
||||
if name.is_empty() || name == "type" {
|
||||
return Err(VmError::InvalidArgument("reserved or empty method name"));
|
||||
}
|
||||
let definition = self.payload::<EndorTypeObject>(type_object, "Type")?;
|
||||
if definition.fields.iter().any(|field| field.name == name) {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"method conflicts with class field",
|
||||
));
|
||||
}
|
||||
let mut methods = definition.methods.clone();
|
||||
methods.insert(name, function);
|
||||
let updated = EndorTypeObject {
|
||||
name: definition.name.clone(),
|
||||
base: definition.base,
|
||||
constructor: definition.constructor,
|
||||
methods,
|
||||
fields: definition.fields.clone(),
|
||||
};
|
||||
self.heap.replace_payload(type_object, updated)
|
||||
}
|
||||
|
||||
/// Atomically build a source class, including inherited constructor fields.
|
||||
/// Native allocation needs its own protocol; never forge a Record payload
|
||||
/// claiming to be a native visual instance.
|
||||
pub fn new_class(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
base: ObjectRef,
|
||||
own_fields: Vec<ClassField>,
|
||||
methods: BTreeMap<String, ObjectRef>,
|
||||
) -> Result<ObjectRef, VmError> {
|
||||
let definition = self.payload::<EndorTypeObject>(base, "Type")?;
|
||||
if !matches!(definition.constructor, TypeConstructor::Record) {
|
||||
return Err(VmError::UnsupportedOperation(
|
||||
"source subclass of native or abstract allocator",
|
||||
));
|
||||
}
|
||||
let mut fields = definition.fields.clone();
|
||||
for field in own_fields {
|
||||
if field.name.is_empty()
|
||||
|| field.name == "type"
|
||||
|| fields.iter().any(|old| old.name == field.name)
|
||||
|| self.lookup_method(base, &field.name)?.is_some()
|
||||
{
|
||||
return Err(VmError::InvalidArgument(
|
||||
"duplicate or reserved class field",
|
||||
));
|
||||
}
|
||||
fields.push(field);
|
||||
}
|
||||
for (name, method) in &methods {
|
||||
self.object(*method)?;
|
||||
if name.is_empty() || name == "type" || fields.iter().any(|field| field.name == *name) {
|
||||
return Err(VmError::InvalidArgument(
|
||||
"method conflicts with class field",
|
||||
));
|
||||
}
|
||||
}
|
||||
let metatype = self
|
||||
.type_of(base)?
|
||||
.ok_or(VmError::InvalidArgument("base type has no metatype"))?;
|
||||
let name = name.into();
|
||||
if name.is_empty() {
|
||||
return Err(VmError::InvalidArgument("type name must not be empty"));
|
||||
}
|
||||
let class = self.allocate(EndorTypeObject {
|
||||
name,
|
||||
base: Some(base),
|
||||
constructor: TypeConstructor::Record,
|
||||
methods,
|
||||
fields,
|
||||
})?;
|
||||
if let Err(error) = self.attach_type(class, metatype) {
|
||||
self.release(class)?;
|
||||
return Err(error);
|
||||
}
|
||||
Ok(class)
|
||||
}
|
||||
|
||||
pub fn lookup_method(
|
||||
&self,
|
||||
type_object: ObjectRef,
|
||||
name: &str,
|
||||
) -> Result<Option<ObjectRef>, VmError> {
|
||||
let mut current = Some(type_object);
|
||||
while let Some(type_object) = current {
|
||||
let definition = self.payload::<EndorTypeObject>(type_object, "Type")?;
|
||||
if let Some(function) = definition.methods.get(name) {
|
||||
return Ok(Some(*function));
|
||||
}
|
||||
current = definition.base;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Source positional parameters after receiver binding. None means the
|
||||
/// callable uses a native protocol without a source signature.
|
||||
pub fn positional_parameter_count(
|
||||
&self,
|
||||
function: ObjectRef,
|
||||
) -> Result<Option<usize>, VmError> {
|
||||
let mut current = function;
|
||||
let mut receivers = 0usize;
|
||||
loop {
|
||||
let payload = self.object(current)?.payload();
|
||||
if let Some(bound) = payload.as_any().downcast_ref::<BoundMethod>() {
|
||||
receivers = receivers
|
||||
.checked_add(1)
|
||||
.ok_or(VmError::ExecutionLimit("bound receivers"))?;
|
||||
current = bound.function;
|
||||
} else if let Some(function) = payload
|
||||
.as_any()
|
||||
.downcast_ref::<crate::EndorFunctionObject>()
|
||||
{
|
||||
let count = self
|
||||
.payload::<crate::EndorCodeObject>(function.code(), "code")?
|
||||
.code()
|
||||
.parameters()
|
||||
.len();
|
||||
return count
|
||||
.checked_sub(receivers)
|
||||
.map(Some)
|
||||
.ok_or(VmError::InvalidArgument(
|
||||
"method receiver exceeds parameter count",
|
||||
));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one owned root, including for a newly bound inherited method.
|
||||
pub fn bind_attr(&mut self, receiver: ObjectRef, name: &str) -> Result<Value, VmError> {
|
||||
match self.lookup_attr(receiver, name)? {
|
||||
AttributeLookup::Value(value) => {
|
||||
if let Value::Object(object) = value {
|
||||
self.retain(object)?;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
AttributeLookup::Method { function, receiver } => self
|
||||
.allocate(BoundMethod { function, receiver })
|
||||
.map(Value::Object),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lookup_attr(
|
||||
&self,
|
||||
receiver: ObjectRef,
|
||||
name: &str,
|
||||
) -> Result<AttributeLookup, VmError> {
|
||||
let object = self.object(receiver)?;
|
||||
if name == "type"
|
||||
&& let Some(type_object) = self.type_of(receiver)?
|
||||
{
|
||||
return Ok(AttributeLookup::Value(Value::Object(type_object)));
|
||||
}
|
||||
if let Some(definition) = object.payload().as_any().downcast_ref::<EndorTypeObject>() {
|
||||
let metadata = match name {
|
||||
"name" => Some(Value::Text(definition.name.clone())),
|
||||
"base" => Some(definition.base.map(Value::Object).unwrap_or(Value::Nil)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(value) = metadata {
|
||||
return Ok(AttributeLookup::Value(value));
|
||||
}
|
||||
if let Some(function) = self.lookup_method(receiver, name)? {
|
||||
return Ok(AttributeLookup::Value(Value::Object(function)));
|
||||
}
|
||||
}
|
||||
match self.heap.get_attr(receiver, name) {
|
||||
Ok(value) => return Ok(AttributeLookup::Value(value)),
|
||||
Err(VmError::AttributeNotFound(_) | VmError::UnsupportedOperation("attributes")) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
if let Some(type_object) = self.type_of(receiver)? {
|
||||
if let Some(function) = self.lookup_method(type_object, name)? {
|
||||
return Ok(AttributeLookup::Method { function, receiver });
|
||||
}
|
||||
return Err(VmError::AttributeNotFound(name.into()));
|
||||
}
|
||||
self.heap
|
||||
.get_attr(receiver, name)
|
||||
.map(AttributeLookup::Value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum AttributeLookup {
|
||||
Value(Value),
|
||||
Method {
|
||||
function: ObjectRef,
|
||||
receiver: ObjectRef,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BoundMethod {
|
||||
receiver: ObjectRef,
|
||||
function: ObjectRef,
|
||||
}
|
||||
impl ObjectPayload for BoundMethod {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"bound-method"
|
||||
}
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
visit(self.receiver);
|
||||
visit(self.function);
|
||||
}
|
||||
fn native_callable(&self) -> Option<Arc<dyn NativeCallable>> {
|
||||
Some(Arc::new(CallBoundMethod))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CallBoundMethod;
|
||||
impl NativeCallable for CallBoundMethod {
|
||||
fn invoke(
|
||||
&self,
|
||||
cx: &mut NativeContext<'_>,
|
||||
arguments: &[Value],
|
||||
) -> Result<NativeResult, VmError> {
|
||||
let bound = cx
|
||||
.store()
|
||||
.payload::<BoundMethod>(cx.callee(), "bound method")?;
|
||||
let mut values = Vec::with_capacity(arguments.len() + 1);
|
||||
values.push(Value::Object(bound.receiver));
|
||||
values.extend_from_slice(arguments);
|
||||
Ok(NativeResult::Invoke {
|
||||
function: bound.function,
|
||||
arguments: values,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user