Files
endorphin-aster-vm/crates/objects/src/object.rs
T

270 lines
7.9 KiB
Rust

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 {
/// 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.
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"))
}
/// Opt-in shared-reference assignment. Returning true preserves the existing
/// object identity after replacing its host-owned target. Ordinary payloads
/// return false and retain normal value assignment. Implementations must
/// validate before mutation and must not add untraced managed-object edges.
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)]
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. 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
}
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 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)) {
if let Some(reference) = self.header.type_object {
visit(reference);
}
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
}
}