diff --git a/README.md b/README.md index 4bc987a..4f41d7e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # Aster VM -Rust 编写的可嵌入语言运行时,当前为第一阶段:启动时引导根类 `Object` 和基础元类 `Type`,并由对象工厂创建类和实例。所有堆对象都有公共对象头和类型句柄,类本身也是语言对象。这里的“顶级类”指根类;“生成器”指运行时引导与对象工厂。 +Rust 编写的可嵌入运行时。当前实现 Endor 的 VM 基础设施:`EndorObject`、稳定对象分配、代际句柄、显式引用计数、循环回收和 Rust 原生操作分发。VM 从空堆启动,暂不实现 Python 的 type/元类体系,也不定义语言根类和继承规则。 -本项目尚无词法/语法前端、字节码执行器、渲染器、GC 或完整元类体系,不承诺 Python 兼容。数据库只保存宿主元数据,不保存或恢复 VM 堆。 +尚无词法/语法前端、字节码执行器或渲染器,不承诺 Python 兼容。数据库只保存宿主元数据,不保存或恢复 VM 堆。对象基础层的逐段说明见 [EndorObject](docs/endor-object.md)。 ## 结构 | crate | 职责 | | --- | --- | -| `aster-core` | 零外部依赖的 `Value`、安全句柄、堆、类、实例、VM 与工厂 | +| `aster-core` | 零外部依赖的 EndorObject、操作接口、引用计数、循环回收与 VM | | `aster-storage` | SQLx PostgreSQL/SQLite 适配、元数据表初始化、读写接口;不依赖内核 | | `aster-cli` | `aster bootstrap` 演示,以及宿主元数据读写 | -| `aster-server` | 最小常驻宿主,仅提供健康检查与根类信息,供 Docker 等方式运行 | +| `aster-server` | 最小常驻宿主,提供健康检查与运行时信息,供 Docker 等方式运行 | 依赖方向:CLI/Server → Core 和 Storage;Core 与 Storage 互不依赖。未来浏览器宿主的网络、DOM、渲染等能力应留在宿主一侧。 @@ -24,19 +24,21 @@ cargo build --workspace --locked cargo run -p aster-cli -- bootstrap ``` -预期输出含 `root=Object parent=None`、`instances_distinct=true heap_objects=5`(Object、Type、Example 与两个实例)。这条命令不读取数据库配置、不打开数据库;即使设置了错误的 `DATABASE_URL` 也不影响引导。 +预期输出含 `runtime=EndorObject type_system=disabled`、`objects_distinct=true heap_objects=2` 和 `released heap_objects=0`。这条命令不读取数据库配置、不打开数据库;即使设置了错误的 `DATABASE_URL` 也不影响运行时演示。 ```rust use aster_core::{Value, Vm}; fn main() -> Result<(), aster_core::VmError> { let mut vm = Vm::new()?; - let point = vm.create_class("Point", vm.root_class())?; - let instance = vm.create_instance(point)?; - vm.set_field(instance, "x", Value::Integer(42))?; - assert_eq!(vm.type_of(instance.into())?, point); - assert_eq!(vm.type_of(point.into())?, vm.type_class()); - assert_eq!(vm.type_of(vm.type_class().into())?, vm.type_class()); + let object = vm.new_object()?; // 分配返回一个宿主持有的强引用。 + vm.set_attr(object, "x", Value::Integer(42))?; + assert_eq!(vm.get_attr(object, "x")?, Value::Integer(42)); + vm.retain(object)?; + vm.release(object)?; + assert_eq!(vm.ref_count(object)?, 1); + vm.release(object)?; // 最后一个强引用释放,对象立即清理。 + assert_eq!(vm.object_count(), 0); Ok(()) } ``` @@ -67,7 +69,7 @@ curl --fail http://127.0.0.1:8080/health curl --fail http://127.0.0.1:8080/bootstrap ``` -`ASTER_LISTEN` 默认 `127.0.0.1:8080`。宿主启动时创建 VM、连接数据库、初始化表并写入 `host.version=0.1.0`;初始化失败即退出。`/health` 实际探测数据库,成功返回 200,失联返回 503;`/bootstrap` 返回 `Object`、无父类、堆对象数 2(Object 和 Type),多次读取不会重复引导。支持 Ctrl-C/Unix SIGTERM 优雅退出。 +`ASTER_LISTEN` 默认 `127.0.0.1:8080`。宿主启动时创建 VM、连接数据库、初始化表并写入 `host.version=0.1.0`;初始化失败即退出。`/health` 实际探测数据库,成功返回 200,失联返回 503;`/bootstrap` 返回 `object_model=EndorObject`、`type_system=false`、`heap_objects=0`,多次读取不分配对象。支持 Ctrl-C/Unix SIGTERM 优雅退出。 这两个端点不提供执行语言、对象写入或业务 API。服务没有认证层;示例 Compose 端口仅绑定宿主回环地址,外部访问由后续宿主部署决定。 @@ -158,13 +160,13 @@ docker stop aster-pg-test ## 当前语义与后续开发 -- Rust 的 `Object` 是统一堆对象结构,包含 `ObjectHeader` 和 `ObjectData`。对象头保存类型句柄;载荷区分 `Class` 元数据和 `Instance` 字段。`Class` 结构本身只是载荷,不再单独代表一个语言对象。`vm.type_of(reference)` 对类和实例都读取同一个公共对象头,替代旧的 `vm.instance(reference)?.class()`。 -- 语言根类 `Object` 唯一且没有父类;它的类型是 `Type`。`Type` 的父类是 Object,类型是 Type 自身。普通类的类型为 Type,实例的类型为其所属类。当前仅支持这一基础元类:禁止直接 `create_instance(Type)` 或继承 Type,自定义元类、类型调用协议与描述符尚未实现。 -- 普通类必须指定已有父类,名称只是标签,无全局命名空间;保留精确名称 `Object` 和 `Type`,其他同名类允许存在。 -- 对外只提供私有字段的 `ObjectRef`/`ClassRef`/`InstanceRef`。句柄包含进程内 VM 身份与槽位,所有访问检查归属、范围、种类;写入对象字段也检查引用归属。句柄不是持久 ID,不支持外部构造或反序列化。 -- 堆仅分配,不单独回收、不复用槽位;随 VM 销毁统一释放。宿主持有的句柄不延长 VM 生命周期。未来 GC 需要另行设计根集、代际槽位或稳定间接句柄,不能直接开始复用当前索引。 -- Nil/布尔/整数/浮点直接放入 `Value`,字符串由 Rust `String` 持有,类和实例使用堆句柄。尚未定义完整语言数值、方法查找、继承执行或异常语义。 +- `EndorObject` 是 Rust 运行时公共对象结构,包含身份、引用计数、原生载荷和托管属性。没有语言类或类型指针;`ObjectPayload` 是 Rust 操作接口,诊断名称不参与语言类型判断。 +- `ObjectRef` 包含私有 VM 身份、槽位和代数。`Box` 保持存活对象地址稳定;槽位复用更新代数,旧句柄不会指向新对象。外部不构造堆索引,也不操作裸指针。 +- 分配返回一个外部强引用;复制句柄或含句柄的 `Value` 不增加计数。额外持有使用 `retain`,结束持有使用 `release`。对象属性和载荷声明的边各自持有内部强引用;读属性与原生调用返回借用值,需长期持有时显式 retain。 +- 最后一个强引用释放时,迭代清理依赖对象并调用 Rust Drop。`collect_cycles()` 从外部强引用追踪,显式回收不可达环;不会自动定时执行。销毁 VM 时统一释放剩余对象。 +- 支持 repr/str、身份与内容比较、哈希、真值、属性读写删除、原生函数调用。操作由 Rust 载荷实现;不支持的操作返回错误。原生调用可返回标量或已有对象的借用句柄,当前不能在回调中修改 VM;没有语言函数执行器。 +- 标量与字符串继续直接保存在 `Value`,不要求全部值装箱。语言根类、继承、元类、描述符、语言异常及 VM 堆持久化均留待以后定义。 -下一阶段建议先确定字段/方法查找与宿主调用边界,再设计可测的最小指令执行循环;等对象可达性规则明确后引入 GC。详见 [架构](docs/architecture.md) 与 [开发交接](docs/development.md)。 +下一阶段可围绕执行栈的强引用根管理和可变 VM 调用上下文推进执行器;GC 已有手动全堆版本,尚无分代、增量、并行收集或语言终结器/复活语义。详见 [架构](docs/architecture.md)、[对象基础层](docs/endor-object.md) 与 [开发交接](docs/development.md)。 依赖 API 依据 [SQLx 0.8.6 官方文档](https://docs.rs/sqlx/0.8.6/sqlx/) 和 [Axum 官方文档](https://docs.rs/axum/0.8.9/axum/) 实现。SQLx 0.9 要求更高 Rust 版本,本阶段使用与本机工具链兼容的 0.8.6;确切解析版本以锁文件为准。 diff --git a/crates/aster-cli/src/main.rs b/crates/aster-cli/src/main.rs index 0cdc32b..b703ed8 100644 --- a/crates/aster-cli/src/main.rs +++ b/crates/aster-cli/src/main.rs @@ -27,22 +27,22 @@ async fn run() -> Result<(), Box> { { [] | ["bootstrap"] => { let mut vm = Vm::new()?; - let root = vm.bootstrap(); - let class = vm.create_class("Example", root)?; - let first = vm.create_instance(class)?; - let second = vm.create_instance(class)?; - vm.set_field(first, "message", Value::Text("Hello from Aster VM".into()))?; + let first = vm.new_object()?; + let second = vm.new_object()?; + vm.set_attr( + first, + "message", + Value::Text("Hello from EndorObject".into()), + )?; + println!("runtime=EndorObject type_system=disabled"); println!( - "root={} parent={:?}", - vm.class(root)?.name(), - vm.class(root)?.parent() - ); - println!( - "class={} instances_distinct={} heap_objects={}", - vm.class(class)?.name(), + "objects_distinct={} heap_objects={}", first != second, vm.object_count() ); + vm.release(first)?; + vm.release(second)?; + println!("released heap_objects={}", vm.object_count()); println!("bootstrap is independent of host storage"); } ["help"] | ["--help"] | ["-h"] => println!("{USAGE}"), diff --git a/crates/aster-core/src/heap.rs b/crates/aster-core/src/heap.rs index f841f6e..b04e736 100644 --- a/crates/aster-core/src/heap.rs +++ b/crates/aster-core/src/heap.rs @@ -1,188 +1,100 @@ use std::collections::BTreeMap; +use std::fmt; use std::sync::atomic::{AtomicU64, Ordering}; -use crate::VmError; +use crate::{EndorObject, ObjectHeader, ObjectPayload, Value, VmError}; static NEXT_HEAP_ID: AtomicU64 = AtomicU64::new(1); -/// Opaque process-local identity. Cannot be constructed or deserialized by callers. -/// Handles do not keep the VM alive; every dereference goes through its owner VM. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// 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, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ClassRef(pub(crate) ObjectRef); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct InstanceRef(pub(crate) ObjectRef); - -impl From for ObjectRef { - fn from(value: ClassRef) -> Self { - value.0 - } -} - -impl From for ObjectRef { - fn from(value: InstanceRef) -> Self { - value.0 - } -} - -/// Primitive values are immediate; only classes and instances live on the heap. -#[derive(Debug, Clone, PartialEq)] -pub enum Value { - Nil, - Bool(bool), - Integer(i64), - Float(f64), - Text(String), - Object(ObjectRef), -} - -/// Immutable class metadata, stored in a language object's payload. -/// This Rust structure is not a language object on its own. -#[derive(Debug)] -pub struct Class { - pub(crate) name: String, - pub(crate) parent: Option, -} - -impl Class { - pub fn name(&self) -> &str { - &self.name - } - - pub fn parent(&self) -> Option { - self.parent +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)] -pub struct Instance { - pub(crate) fields: BTreeMap, +struct Slot { + generation: u64, + object: Option>, } -impl Instance { - pub fn field(&self, name: &str) -> Option<&Value> { - self.fields.get(name) - } -} - -/// Common header for every heap object, including class objects. -/// The class handle is immutable and is always local to the owning VM. -#[derive(Debug)] -pub struct ObjectHeader { - class: ClassRef, -} - -impl ObjectHeader { - pub fn class(&self) -> ClassRef { - self.class - } -} - -#[derive(Debug)] -pub enum ObjectData { - Class(Class), - Instance(Instance), -} - -/// A language object consists of a common header and a kind-specific payload. -/// Object links use checked handles rather than addresses into the arena. -#[derive(Debug)] -pub struct Object { - header: ObjectHeader, - pub(crate) data: ObjectData, -} - -impl Object { - pub(crate) fn new(class: ClassRef, data: ObjectData) -> Self { - Self { - header: ObjectHeader { class }, - data, - } - } - - pub fn header(&self) -> &ObjectHeader { - &self.header - } - - pub fn data(&self) -> &ObjectData { - &self.data - } -} - -/// Allocation-only arena. No deletion or slot reuse until a GC model is chosen. #[derive(Debug)] pub(crate) struct Heap { id: u64, - objects: Vec, + slots: Vec, + free: Vec, + live: usize, } impl Heap { - /// Builds both foundational classes before exposing the heap. The two - /// private handles permit Type's self-reference without null placeholders. - pub(crate) fn bootstrap() -> Result<(Self, ClassRef, ClassRef), VmError> { - let mut heap = Self::new()?; - let root = ClassRef(ObjectRef { - heap_id: heap.id, - index: 0, - }); - let type_class = ClassRef(ObjectRef { - heap_id: heap.id, - index: 1, - }); - heap.allocate(Object::new( - type_class, - ObjectData::Class(Class { - name: "Object".into(), - parent: None, - }), - )); - heap.allocate(Object::new( - type_class, - ObjectData::Class(Class { - name: "Type".into(), - parent: Some(root), - }), - )); - Ok((heap, root, type_class)) - } - pub(crate) fn new() -> Result { let id = NEXT_HEAP_ID .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)) .map_err(|_| VmError::IdentityExhausted)?; Ok(Self { id, - objects: Vec::new(), + slots: Vec::new(), + free: Vec::new(), + live: 0, }) } - pub(crate) fn allocate(&mut self, object: Object) -> ObjectRef { + pub(crate) fn allocate( + &mut self, + payload: Box, + ) -> Result { + 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: self.objects.len(), + index, + generation: self.slots[index].generation, }; - self.objects.push(object); - reference + 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<&Object, VmError> { + pub(crate) fn get(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> { self.validate_owner(reference)?; - self.objects + self.slots .get(reference.index) + .filter(|slot| slot.generation == reference.generation) + .and_then(|slot| slot.object.as_deref()) .ok_or(VmError::InvalidHandle) } - pub(crate) fn get_mut(&mut self, reference: ObjectRef) -> Result<&mut Object, VmError> { - self.validate_owner(reference)?; - self.objects - .get_mut(reference.index) - .ok_or(VmError::InvalidHandle) + 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> { @@ -192,68 +104,257 @@ impl Heap { Ok(()) } + fn prepare_increments( + &self, + references: &[ObjectRef], + ) -> Result, VmError> { + let mut counts = BTreeMap::::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) { + 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 { + 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) { + 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 { + 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) { + 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.objects.len() + self.live } } #[cfg(test)] mod tests { use super::*; + use crate::Record; #[test] - fn invalid_index_is_an_error_for_both_access_paths() { + fn invalid_slots_and_generations_are_rejected() { let mut heap = Heap::new().unwrap(); + let reference = heap.allocate(Box::new(Record)).unwrap(); let invalid = ObjectRef { - heap_id: heap.id, index: usize::MAX, + ..reference + }; + let stale = ObjectRef { + generation: 1, + ..reference }; assert!(matches!(heap.get(invalid), Err(VmError::InvalidHandle))); - assert!(matches!(heap.get_mut(invalid), Err(VmError::InvalidHandle))); + assert!(matches!(heap.get_mut(stale), Err(VmError::InvalidHandle))); } #[test] - fn typed_handles_are_checked_at_runtime_too() { - let mut vm = crate::Vm::new().unwrap(); - let root = vm.root_class(); - let instance = vm.create_instance(root).unwrap(); - assert!(matches!( - vm.class(ClassRef(instance.0)), - Err(VmError::ExpectedClass) - )); - assert!(matches!( - vm.instance(InstanceRef(root.0)), - Err(VmError::ExpectedInstance) - )); - } - - #[test] - fn type_lookup_rejects_an_invalid_slot() { - let vm = crate::Vm::new().unwrap(); - let invalid = ObjectRef { - heap_id: vm.root_class().0.heap_id, - index: usize::MAX, + 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 }; - assert_eq!(vm.type_of(invalid), Err(VmError::InvalidHandle)); + 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 invalid_value_reference_cannot_replace_a_field() { - let mut vm = crate::Vm::new().unwrap(); - let root = vm.root_class(); - let instance = vm.create_instance(root).unwrap(); - vm.set_field(instance, "field", Value::Integer(1)).unwrap(); - let invalid = ObjectRef { - heap_id: root.0.heap_id, - index: usize::MAX, - }; + 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!( - vm.set_field(instance, "field", Value::Object(invalid)), - Err(VmError::InvalidHandle) - ); - assert_eq!( - vm.instance(instance).unwrap().field("field"), - Some(&Value::Integer(1)) + heap.set_attr(owner, "edge".into(), Value::Object(target)), + Err(VmError::RefCountOverflow) ); + assert!(heap.get(owner).unwrap().attributes.is_empty()); } } diff --git a/crates/aster-core/src/lib.rs b/crates/aster-core/src/lib.rs index b2cc9ad..ed503f1 100644 --- a/crates/aster-core/src/lib.rs +++ b/crates/aster-core/src/lib.rs @@ -1,20 +1,25 @@ -//! Embeddable object runtime. No database, network, parser or executor dependencies. +//! Embeddable object runtime. No language type system, parser or executor. //! //! ``` //! use aster_core::{Value, Vm}; //! let mut vm = Vm::new()?; -//! let point = vm.create_class("Point", vm.root_class())?; -//! let instance = vm.create_instance(point)?; -//! vm.set_field(instance, "x", Value::Integer(42))?; -//! assert_eq!(vm.type_of(instance.into())?, point); -//! assert_eq!(vm.type_of(point.into())?, vm.type_class()); +//! let object = vm.new_object()?; // One owned external reference. +//! vm.set_attr(object, "x", Value::Integer(42))?; +//! assert_eq!(vm.get_attr(object, "x")?, Value::Integer(42)); +//! vm.retain(object)?; // Own an additional reference. +//! vm.release(object)?; +//! assert_eq!(vm.ref_count(object)?, 1); +//! vm.release(object)?; // The object is destroyed. +//! assert_eq!(vm.object_count(), 0); //! # Ok::<(), aster_core::VmError>(()) //! ``` mod heap; +mod object; mod vm; -pub use heap::{ - Class, ClassRef, Instance, InstanceRef, Object, ObjectData, ObjectHeader, ObjectRef, Value, +pub use heap::ObjectRef; +pub use object::{ + CompareOp, EndorObject, NativeCall, NativeFunction, ObjectHeader, ObjectPayload, Record, Value, }; pub use vm::{Vm, VmError}; diff --git a/crates/aster-core/src/object.rs b/crates/aster-core/src/object.rs new file mode 100644 index 0000000..fb45a05 --- /dev/null +++ b/crates/aster-core/src/object.rs @@ -0,0 +1,210 @@ +use std::any::Any; +use std::collections::BTreeMap; +use std::collections::hash_map::DefaultHasher; +use std::fmt; +use std::hash::{Hash, Hasher}; + +use crate::{ObjectRef, VmError}; + +/// Immediate values stay outside the object heap. Object values are borrowed +/// handles: copying or cloning one does not retain an external strong reference. +#[derive(Debug, Clone, PartialEq)] +pub enum Value { + Nil, + Bool(bool), + Integer(i64), + Float(f64), + 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 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 { + Ok(format!("<{} {identity}>", self.kind_name())) + } + + fn str(&self, identity: ObjectRef) -> Result { + self.repr(identity) + } + + fn is_true(&self) -> Result { + Ok(true) + } + + fn hash(&self, identity: ObjectRef) -> Result { + 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, VmError> { + Ok(None) + } + + /// Object arguments and object results are borrowed handles. Native calls + /// compute values without mutating the VM; this is not a language executor. + fn call(&self, _arguments: &[Value]) -> Result { + 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 NativeCall = fn(&[Value]) -> Result; + +#[derive(Debug)] +pub struct NativeFunction { + name: String, + function: NativeCall, +} + +impl NativeFunction { + pub fn new(name: impl Into, function: NativeCall) -> Self { + Self { + name: name.into(), + function, + } + } +} + +impl ObjectPayload for NativeFunction { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "native-function" + } + + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(format!("", self.name)) + } + + fn call(&self, arguments: &[Value]) -> Result { + (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 + } + + 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, + pub(crate) attributes: BTreeMap, + pub(crate) payload_edges: Vec, +} + +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; 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 { + let mut edges = Vec::new(); + self.trace(&mut |reference| edges.push(reference)); + edges + } +} diff --git a/crates/aster-core/src/vm.rs b/crates/aster-core/src/vm.rs index 145261f..d819440 100644 --- a/crates/aster-core/src/vm.rs +++ b/crates/aster-core/src/vm.rs @@ -1,168 +1,166 @@ -use std::collections::BTreeMap; use std::fmt; use crate::heap::Heap; -use crate::{Class, ClassRef, Instance, InstanceRef, Object, ObjectData, ObjectRef, Value}; +use crate::{CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum VmError { ForeignHandle, InvalidHandle, - ExpectedClass, - ExpectedInstance, - EmptyClassName, - ReservedClassName, - UnsupportedMetaclass, IdentityExhausted, + RefCountOverflow, + NoOwnedReference, + AttributeNotFound(String), + UnsupportedOperation(&'static str), + InvalidArgument(&'static str), } impl fmt::Display for VmError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::ForeignHandle => "handle belongs to another VM", - Self::InvalidHandle => "handle does not identify a live object", - Self::ExpectedClass => "object is not a class", - Self::ExpectedInstance => "object is not an instance", - Self::EmptyClassName => "class name cannot be blank", - Self::ReservedClassName => "Object and Type are reserved for the bootstrap classes", - Self::UnsupportedMetaclass => "Type only supports class creation through create_class", - Self::IdentityExhausted => "process-local VM identities exhausted", - }) + 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), + } } } impl std::error::Error for VmError {} -/// Owns its heap, the root Object class and its Type metaclass. -/// Not Clone: handles have one owner. +/// Runtime owner. Construction creates an empty heap, without language classes, +/// metaclasses or a Python type system. Dropping it releases all host resources. #[derive(Debug)] pub struct Vm { heap: Heap, - root: ClassRef, - type_class: ClassRef, } impl Vm { - /// Bootstraps Object and Type before returning. Does not perform host I/O. pub fn new() -> Result { - let (heap, root, type_class) = Heap::bootstrap()?; - Ok(Self { - heap, - root, - type_class, - }) - } - - /// Idempotent: construction has already performed bootstrap exactly once. - pub fn bootstrap(&mut self) -> ClassRef { - self.root - } - - pub fn root_class(&self) -> ClassRef { - self.root - } - - pub fn type_class(&self) -> ClassRef { - self.type_class + Ok(Self { heap: Heap::new()? }) } pub fn object_count(&self) -> usize { self.heap.len() } - /// All user classes have an existing parent. Class names are labels rather - /// than a global registry; duplicate non-reserved names are permitted. - pub fn create_class( - &mut self, - name: impl Into, - parent: ClassRef, - ) -> Result { - self.class(parent)?; - if parent == self.type_class { - return Err(VmError::UnsupportedMetaclass); - } - let name = name.into(); - if name.trim().is_empty() { - return Err(VmError::EmptyClassName); - } - if name == "Object" || name == "Type" { - return Err(VmError::ReservedClassName); - } - Ok(ClassRef(self.heap.allocate(Object::new( - self.type_class, - ObjectData::Class(Class { - name, - parent: Some(parent), - }), - )))) + /// 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 { + self.heap.allocate(Box::new(payload)) } - pub fn create_instance(&mut self, class: ClassRef) -> Result { - self.class(class)?; - if class == self.type_class { - return Err(VmError::UnsupportedMetaclass); - } - Ok(InstanceRef(self.heap.allocate(Object::new( - class, - ObjectData::Instance(Instance { - fields: BTreeMap::new(), - }), - )))) + pub fn new_object(&mut self) -> Result { + self.allocate(Record) } - pub fn object(&self, reference: ObjectRef) -> Result<&Object, VmError> { + pub fn object(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> { self.heap.get(reference) } - /// Reads the same common header for class objects and ordinary instances. - pub fn type_of(&self, reference: ObjectRef) -> Result { - Ok(self.heap.get(reference)?.header().class()) + pub fn retain(&mut self, reference: ObjectRef) -> Result<(), VmError> { + self.heap.retain(reference) } - pub fn class(&self, reference: ClassRef) -> Result<&Class, VmError> { - match self.heap.get(reference.0)?.data() { - ObjectData::Class(class) => Ok(class), - ObjectData::Instance(_) => Err(VmError::ExpectedClass), + pub fn release(&mut self, reference: ObjectRef) -> Result<(), VmError> { + self.heap.release(reference) + } + + pub fn ref_count(&self, reference: ObjectRef) -> Result { + Ok(self.heap.get(reference)?.header().ref_count()) + } + + pub fn is_same(&self, left: ObjectRef, right: ObjectRef) -> Result { + self.heap.get(left)?; + self.heap.get(right)?; + Ok(left == right) + } + + pub fn repr(&self, reference: ObjectRef) -> Result { + self.heap.get(reference)?.payload().repr(reference) + } + + pub fn str(&self, reference: ObjectRef) -> Result { + self.heap.get(reference)?.payload().str(reference) + } + + pub fn is_true(&self, reference: ObjectRef) -> Result { + self.heap.get(reference)?.payload().is_true() + } + + pub fn hash(&self, reference: ObjectRef) -> Result { + self.heap.get(reference)?.payload().hash(reference) + } + + pub fn compare( + &self, + left: ObjectRef, + right: ObjectRef, + operation: CompareOp, + ) -> Result { + 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")), } } - pub fn instance(&self, reference: InstanceRef) -> Result<&Instance, VmError> { - match self.heap.get(reference.0)?.data() { - ObjectData::Instance(instance) => Ok(instance), - ObjectData::Class(_) => Err(VmError::ExpectedInstance), - } + /// 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 { + self.heap.get_attr(reference, name) } - /// Converts a generic reference only after validating ownership and kind. - pub fn as_class(&self, reference: ObjectRef) -> Result { - let typed = ClassRef(reference); - self.class(typed)?; - Ok(typed) - } - - pub fn as_instance(&self, reference: ObjectRef) -> Result { - let typed = InstanceRef(reference); - self.instance(typed)?; - Ok(typed) - } - - /// Checks both destination and referenced value before changing any field. - pub fn set_field( + /// 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, - instance: InstanceRef, + reference: ObjectRef, name: impl Into, value: Value, ) -> Result<(), VmError> { - self.instance(instance)?; - if let Value::Object(reference) = &value { + 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) + } + + /// Results are borrowed values; native operations cannot mutate this VM. + pub fn call(&self, reference: ObjectRef, arguments: &[Value]) -> Result { + let payload = self.heap.get(reference)?.payload(); + for argument in arguments { + self.validate_value(argument)?; + } + let result = payload.call(arguments)?; + self.validate_value(&result)?; + Ok(result) + } + + fn validate_value(&self, value: &Value) -> Result<(), VmError> { + if let Value::Object(reference) = value { self.heap.get(*reference)?; } - match &mut self.heap.get_mut(instance.0)?.data { - ObjectData::Instance(instance) => { - instance.fields.insert(name.into(), value); - Ok(()) - } - ObjectData::Class(_) => Err(VmError::ExpectedInstance), - } + Ok(()) + } + + /// Explicit full collection of unreachable cycles, using external strong + /// references as roots. A tracing execution stack can be added later. + pub fn collect_cycles(&mut self) -> usize { + self.heap.collect_cycles() } } diff --git a/crates/aster-core/tests/runtime.rs b/crates/aster-core/tests/runtime.rs index 7425262..12a4e35 100644 --- a/crates/aster-core/tests/runtime.rs +++ b/crates/aster-core/tests/runtime.rs @@ -1,172 +1,461 @@ -use aster_core::{ObjectData, Value, Vm, VmError}; +use std::any::Any; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; -#[test] -fn bootstrap_is_unique_and_root_has_no_parent() { - let mut vm = Vm::new().unwrap(); - let root = vm.root_class(); - assert_eq!(vm.bootstrap(), root); - assert_eq!(vm.bootstrap(), root); - assert_eq!(vm.object_count(), 2); - assert_eq!(vm.class(root).unwrap().name(), "Object"); - assert_eq!(vm.class(root).unwrap().parent(), None); +use aster_core::{CompareOp, NativeFunction, ObjectPayload, ObjectRef, Record, Value, Vm, VmError}; + +#[derive(Debug)] +struct Resource { + drops: Arc, + edges: Vec, } -#[test] -fn bootstrap_types_have_headers_and_distinct_inheritance_links() { - let vm = Vm::new().unwrap(); - let root = vm.root_class(); - let type_class = vm.type_class(); - assert_ne!(root, type_class); - assert_eq!(vm.class(type_class).unwrap().name(), "Type"); - assert_eq!(vm.class(type_class).unwrap().parent(), Some(root)); - assert_eq!(vm.type_of(root.into()).unwrap(), type_class); - assert_eq!(vm.type_of(type_class.into()).unwrap(), type_class); - for class in [root, type_class] { - let object = vm.object(class.into()).unwrap(); - assert_eq!(object.header().class(), type_class); - assert!(matches!(object.data(), ObjectData::Class(_))); - assert_eq!(vm.as_class(class.into()).unwrap(), class); - assert_eq!(vm.as_instance(class.into()), Err(VmError::ExpectedInstance)); +impl Drop for Resource { + fn drop(&mut self) { + self.drops.fetch_add(1, Ordering::SeqCst); } } -#[test] -fn factories_preserve_class_links_and_distinct_identity() { - let mut vm = Vm::new().unwrap(); - let root = vm.root_class(); - let class = vm.create_class("Point", root).unwrap(); - let child = vm.create_class("ColoredPoint", class).unwrap(); - let first = vm.create_instance(class).unwrap(); - let second = vm.create_instance(class).unwrap(); - let plain = vm.create_instance(root).unwrap(); - assert_ne!(first, second); - assert_eq!(vm.class(child).unwrap().parent(), Some(class)); - assert_eq!(vm.type_of(class.into()).unwrap(), vm.type_class()); - assert_eq!(vm.type_of(child.into()).unwrap(), vm.type_class()); - assert_eq!(vm.type_of(first.into()).unwrap(), class); - assert_eq!(vm.type_of(plain.into()).unwrap(), root); - let object = vm.object(first.into()).unwrap(); - assert_eq!(object.header().class(), class); - assert!(matches!(object.data(), ObjectData::Instance(_))); - vm.set_field(first, "x", Value::Integer(1)).unwrap(); - vm.set_field(first, "next", Value::Object(second.into())) - .unwrap(); - assert_eq!( - vm.instance(first).unwrap().field("x"), - Some(&Value::Integer(1)) - ); - assert_eq!(vm.instance(second).unwrap().field("x"), None); - assert_eq!(vm.as_class(class.into()).unwrap(), class); - assert_eq!(vm.as_instance(first.into()).unwrap(), first); - assert_eq!(vm.as_instance(class.into()), Err(VmError::ExpectedInstance)); - assert_eq!(vm.as_class(first.into()), Err(VmError::ExpectedClass)); +impl ObjectPayload for Resource { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "resource" + } + + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + for reference in &self.edges { + visit(*reference); + } + } + + fn allows_attributes(&self) -> bool { + true + } +} + +#[derive(Debug)] +struct Number(i64); + +impl ObjectPayload for Number { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "number" + } + + fn repr(&self, _identity: ObjectRef) -> Result { + Ok(self.0.to_string()) + } + + fn is_true(&self) -> Result { + Ok(self.0 != 0) + } + + fn hash(&self, _identity: ObjectRef) -> Result { + Ok(self.0 as u64) + } + + fn compare( + &self, + other: &dyn ObjectPayload, + operation: CompareOp, + ) -> Result, VmError> { + let Some(other) = other.as_any().downcast_ref::() else { + return Ok(None); + }; + Ok(Some(match operation { + CompareOp::Eq => self.0 == other.0, + CompareOp::Ne => self.0 != other.0, + CompareOp::Lt => self.0 < other.0, + CompareOp::Le => self.0 <= other.0, + CompareOp::Gt => self.0 > other.0, + CompareOp::Ge => self.0 >= other.0, + })) + } +} + +fn resource(vm: &mut Vm, drops: &Arc, edges: Vec) -> ObjectRef { + vm.allocate(Resource { + drops: drops.clone(), + edges, + }) + .unwrap() } #[test] -fn foreign_handles_are_rejected_without_mutation() { +fn starts_empty_without_language_classes_or_a_type_system() { + let vm = Vm::new().unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn defaults_provide_identity_representation_hash_and_truth() { + let mut vm = Vm::new().unwrap(); + let first = vm.new_object().unwrap(); + let second = vm.new_object().unwrap(); + assert_eq!(vm.object(first).unwrap().header().identity(), first); + assert!(vm.is_same(first, first).unwrap()); + assert!(!vm.is_same(first, second).unwrap()); + assert!(vm.repr(first).unwrap().contains(&first.to_string())); + assert_eq!(vm.repr(first).unwrap(), vm.str(first).unwrap()); + assert_eq!(vm.hash(first).unwrap(), vm.hash(first).unwrap()); + assert!(vm.is_true(first).unwrap()); + assert!(vm.compare(first, first, CompareOp::Eq).unwrap()); + assert!(vm.compare(first, second, CompareOp::Ne).unwrap()); + assert_eq!( + vm.compare(first, second, CompareOp::Lt), + Err(VmError::UnsupportedOperation("ordering")) + ); + assert_eq!( + vm.call(first, &[]), + Err(VmError::UnsupportedOperation("call")) + ); +} + +#[test] +fn explicit_references_control_lifetime_and_copies_are_borrowed() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let owned = resource(&mut vm, &drops, vec![]); + let borrowed = owned; + assert_eq!(vm.ref_count(borrowed).unwrap(), 1); + vm.retain(owned).unwrap(); + assert_eq!(vm.ref_count(owned).unwrap(), 2); + vm.release(owned).unwrap(); + assert_eq!(drops.load(Ordering::SeqCst), 0); + vm.release(owned).unwrap(); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(vm.object_count(), 0); + assert!(matches!(vm.object(borrowed), Err(VmError::InvalidHandle))); + assert_eq!(vm.release(borrowed), Err(VmError::InvalidHandle)); +} + +#[test] +fn attributes_keep_objects_alive_and_delete_releases_them() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let owner = vm.new_object().unwrap(); + let child = resource(&mut vm, &drops, vec![]); + vm.set_attr(owner, "child", Value::Object(child)).unwrap(); + assert_eq!(vm.ref_count(child).unwrap(), 2); + vm.release(child).unwrap(); + assert_eq!(vm.ref_count(child).unwrap(), 1); + assert_eq!(vm.release(child), Err(VmError::NoOwnedReference)); + assert_eq!(vm.get_attr(owner, "child").unwrap(), Value::Object(child)); + vm.del_attr(owner, "child").unwrap(); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!( + vm.get_attr(owner, "child"), + Err(VmError::AttributeNotFound("child".into())) + ); +} + +#[test] +fn replacing_the_same_edge_does_not_destroy_the_target() { + let mut vm = Vm::new().unwrap(); + let owner = vm.new_object().unwrap(); + let target = vm.new_object().unwrap(); + vm.set_attr(owner, "child", Value::Object(target)).unwrap(); + vm.release(target).unwrap(); + vm.set_attr(owner, "child", Value::Object(target)).unwrap(); + assert_eq!(vm.ref_count(target).unwrap(), 1); + vm.set_attr(owner, "child", Value::Integer(7)).unwrap(); + assert!(matches!(vm.object(target), Err(VmError::InvalidHandle))); + assert_eq!(vm.get_attr(owner, "child").unwrap(), Value::Integer(7)); +} + +#[test] +fn borrowed_attribute_results_can_be_retained_by_the_host() { + let mut vm = Vm::new().unwrap(); + let parent = vm.new_object().unwrap(); + let child = vm.new_object().unwrap(); + vm.set_attr(parent, "child", Value::Object(child)).unwrap(); + vm.release(child).unwrap(); + let Value::Object(borrowed) = vm.get_attr(parent, "child").unwrap() else { + panic!("expected an object"); + }; + vm.retain(borrowed).unwrap(); + vm.release(parent).unwrap(); + assert_eq!(vm.ref_count(borrowed).unwrap(), 1); + vm.release(borrowed).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn failed_foreign_and_stale_writes_preserve_the_old_attribute() { let mut vm = Vm::new().unwrap(); let mut other = Vm::new().unwrap(); - let local = vm.create_instance(vm.root_class()).unwrap(); - let foreign = other.create_instance(other.root_class()).unwrap(); - let count = vm.object_count(); + let owner = vm.new_object().unwrap(); + let foreign = other.new_object().unwrap(); + let stale = vm.new_object().unwrap(); + vm.release(stale).unwrap(); + vm.set_attr(owner, "value", Value::Integer(1)).unwrap(); assert_eq!( - vm.create_class("Alien", other.root_class()), + vm.set_attr(owner, "value", Value::Object(foreign)), Err(VmError::ForeignHandle) ); assert_eq!( - vm.create_instance(other.root_class()), - Err(VmError::ForeignHandle) + vm.set_attr(owner, "value", Value::Object(stale)), + Err(VmError::InvalidHandle) ); - assert!(matches!( - vm.object(foreign.into()), - Err(VmError::ForeignHandle) - )); - assert!(matches!(vm.instance(foreign), Err(VmError::ForeignHandle))); - for reference in [ - foreign.into(), - other.root_class().into(), - other.type_class().into(), - ] { - assert_eq!(vm.type_of(reference), Err(VmError::ForeignHandle)); - } - assert_eq!( - vm.set_field(foreign, "x", Value::Nil), - Err(VmError::ForeignHandle) - ); - vm.set_field(local, "x", Value::Integer(7)).unwrap(); - assert_eq!( - vm.set_field(local, "x", Value::Object(foreign.into())), - Err(VmError::ForeignHandle) - ); - assert_eq!( - vm.instance(local).unwrap().field("x"), - Some(&Value::Integer(7)) - ); - assert_eq!(vm.object_count(), count); + assert_eq!(vm.get_attr(owner, "value").unwrap(), Value::Integer(1)); + assert_eq!(vm.retain(foreign), Err(VmError::ForeignHandle)); + assert_eq!(vm.release(foreign), Err(VmError::ForeignHandle)); + assert_eq!(vm.is_same(owner, foreign), Err(VmError::ForeignHandle)); } #[test] -fn handles_outliving_a_vm_cannot_alias_a_new_vm() { - let old = { - let vm = Vm::new().unwrap(); - vm.root_class() - }; - let vm = Vm::new().unwrap(); - assert_ne!(old, vm.root_class()); - assert!(matches!(vm.class(old), Err(VmError::ForeignHandle))); -} - -#[test] -fn invalid_names_do_not_allocate_and_names_are_not_a_namespace() { +fn released_handles_cannot_alias_new_objects_or_new_vms() { let mut vm = Vm::new().unwrap(); - assert_eq!( - vm.create_class(" ", vm.root_class()), - Err(VmError::EmptyClassName) - ); - for name in ["Object", "Type"] { - assert_eq!( - vm.create_class(name, vm.root_class()), - Err(VmError::ReservedClassName) - ); - } - assert_eq!(vm.object_count(), 2); - let a = vm.create_class("Label", vm.root_class()).unwrap(); - let b = vm.create_class("Label", vm.root_class()).unwrap(); - assert_ne!(a, b); + let old = vm.new_object().unwrap(); + vm.release(old).unwrap(); + let new = vm.new_object().unwrap(); + assert_ne!(old, new); + assert_eq!(vm.repr(old), Err(VmError::InvalidHandle)); + let other = Vm::new().unwrap(); + assert!(matches!(other.object(new), Err(VmError::ForeignHandle))); } #[test] -fn type_cannot_be_used_to_bypass_the_class_factory() { +fn boxed_objects_keep_their_addresses_when_the_slot_vector_grows() { let mut vm = Vm::new().unwrap(); - let type_class = vm.type_class(); - let count = vm.object_count(); - assert_eq!( - vm.create_instance(type_class), - Err(VmError::UnsupportedMetaclass) - ); - assert_eq!( - vm.create_class("CustomType", type_class), - Err(VmError::UnsupportedMetaclass) - ); - assert_eq!(vm.object_count(), count); - assert_eq!(vm.type_of(type_class.into()).unwrap(), type_class); -} - -#[test] -fn handles_remain_valid_when_the_arena_grows() { - let mut vm = Vm::new().unwrap(); - let root = vm.root_class(); - let first = vm.create_instance(root).unwrap(); - vm.set_field(first, "stable", Value::Bool(true)).unwrap(); + let first = vm.new_object().unwrap(); + let address = std::ptr::from_ref(vm.object(first).unwrap()); for _ in 0..4096 { - vm.create_instance(root).unwrap(); + vm.new_object().unwrap(); } - assert_eq!(vm.type_of(first.into()).unwrap(), root); - assert_eq!(vm.type_of(root.into()).unwrap(), vm.type_class()); - assert_eq!(vm.type_of(vm.type_class().into()).unwrap(), vm.type_class()); - assert_eq!( - vm.instance(first).unwrap().field("stable"), - Some(&Value::Bool(true)) - ); - assert_eq!(vm.class(root).unwrap().name(), "Object"); + assert_eq!(std::ptr::from_ref(vm.object(first).unwrap()), address); +} + +#[test] +fn zero_reference_chains_are_released_without_recursive_destructors() { + let mut vm = Vm::new().unwrap(); + let head = vm.new_object().unwrap(); + let mut tail = head; + for _ in 0..4096 { + let next = vm.new_object().unwrap(); + vm.set_attr(tail, "next", Value::Object(next)).unwrap(); + vm.release(next).unwrap(); + tail = next; + } + vm.release(head).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn cycles_remain_while_rooted_and_are_collected_when_unreachable() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let first = resource(&mut vm, &drops, vec![]); + let second = resource(&mut vm, &drops, vec![]); + vm.set_attr(first, "next", Value::Object(second)).unwrap(); + vm.set_attr(second, "next", Value::Object(first)).unwrap(); + vm.release(second).unwrap(); + assert_eq!(vm.collect_cycles(), 0); + vm.release(first).unwrap(); + assert_eq!(vm.object_count(), 2); + assert_eq!(vm.collect_cycles(), 2); + assert_eq!(vm.collect_cycles(), 0); + assert_eq!(drops.load(Ordering::SeqCst), 2); + assert!(matches!(vm.object(first), Err(VmError::InvalidHandle))); + assert!(matches!(vm.object(second), Err(VmError::InvalidHandle))); +} + +#[test] +fn cycle_collection_preserves_rooted_targets_and_updates_counts() { + let mut vm = Vm::new().unwrap(); + let live = vm.new_object().unwrap(); + let cycle = vm.new_object().unwrap(); + vm.set_attr(cycle, "self", Value::Object(cycle)).unwrap(); + vm.set_attr(cycle, "live", Value::Object(live)).unwrap(); + vm.release(cycle).unwrap(); + assert_eq!(vm.ref_count(live).unwrap(), 2); + assert_eq!(vm.collect_cycles(), 1); + assert_eq!(vm.ref_count(live).unwrap(), 1); + vm.release(live).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn payload_edges_are_owned_and_duplicate_edges_are_counted() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let target = vm.new_object().unwrap(); + let holder = resource(&mut vm, &drops, vec![target, target]); + assert_eq!(vm.ref_count(target).unwrap(), 3); + vm.release(target).unwrap(); + vm.release(holder).unwrap(); + assert_eq!(vm.object_count(), 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn payload_edges_participate_in_cycle_tracing() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let record = vm.new_object().unwrap(); + let native = resource(&mut vm, &drops, vec![record]); + vm.set_attr(record, "native", Value::Object(native)) + .unwrap(); + vm.release(record).unwrap(); + vm.release(native).unwrap(); + assert_eq!(vm.collect_cycles(), 2); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn invalid_payload_edges_abort_allocation_without_retaining_valid_edges() { + let drops = Arc::new(AtomicUsize::new(0)); + let mut vm = Vm::new().unwrap(); + let mut other = Vm::new().unwrap(); + let local = vm.new_object().unwrap(); + let foreign = other.new_object().unwrap(); + assert_eq!( + vm.allocate(Resource { + drops: drops.clone(), + edges: vec![local, foreign], + }), + Err(VmError::ForeignHandle) + ); + assert_eq!(vm.ref_count(local).unwrap(), 1); + assert_eq!(vm.object_count(), 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn rust_payload_operations_override_defaults_without_language_types() { + let mut vm = Vm::new().unwrap(); + let zero = vm.allocate(Number(0)).unwrap(); + let first = vm.allocate(Number(42)).unwrap(); + let equal = vm.allocate(Number(42)).unwrap(); + assert!(!vm.is_same(first, equal).unwrap()); + assert!(vm.compare(first, equal, CompareOp::Eq).unwrap()); + assert!(vm.compare(zero, first, CompareOp::Lt).unwrap()); + assert_eq!(vm.hash(first).unwrap(), vm.hash(equal).unwrap()); + assert_eq!(vm.str(first).unwrap(), "42"); + assert!(!vm.is_true(zero).unwrap()); + assert!(vm.is_true(first).unwrap()); +} + +#[derive(Debug)] +struct ReflectedOrdering; + +impl ObjectPayload for ReflectedOrdering { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "reflected-ordering" + } + + fn compare( + &self, + _other: &dyn ObjectPayload, + operation: CompareOp, + ) -> Result, VmError> { + Ok(Some(operation == CompareOp::Gt)) + } + + fn hash(&self, _identity: ObjectRef) -> Result { + Err(VmError::UnsupportedOperation("hash")) + } +} + +#[test] +fn reflected_operations_and_explicitly_unsupported_hashing_are_respected() { + let mut vm = Vm::new().unwrap(); + let left = vm.allocate(Record).unwrap(); + let right = vm.allocate(ReflectedOrdering).unwrap(); + assert!(vm.compare(left, right, CompareOp::Lt).unwrap()); + assert_eq!(vm.hash(right), Err(VmError::UnsupportedOperation("hash"))); +} + +fn add(arguments: &[Value]) -> Result { + match arguments { + [Value::Integer(left), Value::Integer(right)] => left + .checked_add(*right) + .map(Value::Integer) + .ok_or(VmError::InvalidArgument("integer overflow")), + _ => Err(VmError::InvalidArgument("expected two integers")), + } +} + +#[test] +fn native_calls_dispatch_validate_arguments_and_propagate_errors() { + let mut vm = Vm::new().unwrap(); + let function = vm.allocate(NativeFunction::new("add", add)).unwrap(); + assert_eq!(vm.repr(function).unwrap(), ""); + assert_eq!( + vm.call(function, &[Value::Integer(2), Value::Integer(3)]) + .unwrap(), + Value::Integer(5) + ); + assert_eq!( + vm.call(function, &[]), + Err(VmError::InvalidArgument("expected two integers")) + ); + assert_eq!( + vm.call(function, &[Value::Integer(i64::MAX), Value::Integer(1)]), + Err(VmError::InvalidArgument("integer overflow")) + ); + assert_eq!( + vm.set_attr(function, "x", Value::Nil), + Err(VmError::UnsupportedOperation("attributes")) + ); + let mut other = Vm::new().unwrap(); + let foreign = other.new_object().unwrap(); + assert_eq!( + vm.call(function, &[Value::Object(foreign)]), + Err(VmError::ForeignHandle) + ); +} + +#[derive(Debug)] +struct ReturnsObject(ObjectRef); + +impl ObjectPayload for ReturnsObject { + fn as_any(&self) -> &dyn Any { + self + } + + fn kind_name(&self) -> &'static str { + "invalid-return" + } + + fn call(&self, _arguments: &[Value]) -> Result { + Ok(Value::Object(self.0)) + } +} + +#[test] +fn native_results_cannot_smuggle_foreign_or_dead_handles_into_the_vm() { + let mut vm = Vm::new().unwrap(); + let mut other = Vm::new().unwrap(); + let foreign = other.new_object().unwrap(); + let invalid_return = vm.allocate(ReturnsObject(foreign)).unwrap(); + assert_eq!(vm.call(invalid_return, &[]), Err(VmError::ForeignHandle)); + let dead = vm.new_object().unwrap(); + vm.release(dead).unwrap(); + let invalid_return = vm.allocate(ReturnsObject(dead)).unwrap(); + assert_eq!(vm.call(invalid_return, &[]), Err(VmError::InvalidHandle)); +} + +#[test] +fn dropping_the_vm_cleans_up_rooted_objects_and_uncollected_cycles() { + let drops = Arc::new(AtomicUsize::new(0)); + { + let mut vm = Vm::new().unwrap(); + let rooted = resource(&mut vm, &drops, vec![]); + let cycle = resource(&mut vm, &drops, vec![]); + vm.set_attr(cycle, "self", Value::Object(cycle)).unwrap(); + vm.release(cycle).unwrap(); + assert_eq!(vm.ref_count(rooted).unwrap(), 1); + } + assert_eq!(drops.load(Ordering::SeqCst), 2); } diff --git a/crates/aster-server/src/main.rs b/crates/aster-server/src/main.rs index c0643fd..a38357f 100644 --- a/crates/aster-server/src/main.rs +++ b/crates/aster-server/src/main.rs @@ -22,8 +22,8 @@ struct Health { #[derive(Serialize)] struct BootstrapInfo { - root_class: String, - root_has_parent: bool, + object_model: &'static str, + type_system: bool, heap_objects: usize, } @@ -81,13 +81,9 @@ async fn health(State(state): State) -> (StatusCode, Json) { } async fn bootstrap(State(state): State) -> Json { - let root = state - .vm - .class(state.vm.root_class()) - .expect("VM construction guarantees a root class"); Json(BootstrapInfo { - root_class: root.name().into(), - root_has_parent: root.parent().is_some(), + object_model: "EndorObject", + type_system: false, heap_objects: state.vm.object_count(), }) } diff --git a/docs/architecture.md b/docs/architecture.md index e6bc02c..e12f1dc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,29 +2,37 @@ ## 内核 -`Vm` 拥有私有 `Heap`。堆中每个条目都是统一的 Rust `Object`:公共 `ObjectHeader` 保存类型句柄,`ObjectData` 保存类元数据或实例字段。`Class` 只是类载荷,不是独立于公共对象头的语言对象。实例不再重复保存类型,统一通过 `vm.type_of(ObjectRef)` 查询对象头;`vm.class`/`vm.instance` 读取相应载荷。 +当前任务是 Endor 的虚拟机实现层。`Vm::new()` 只创建空堆,不加载语言根类,不分配 Object/Type,也没有类创建、继承或元类 API。`EndorObject` 是公共运行时结构,不能与语言根类混为一谈。 -`Vm::new` 通过私有 `Heap::bootstrap` 一次性建立 Object 和 Type。Object 无父类,类型为 Type;Type 的父类为 Object,类型为自身。初始化器在独占的空堆中计算两个槽位的私有句柄并填入完整对象,不使用裸指针或可空的临时类型引用;完整堆建立后才返回。不存在可观察的“半初始化 VM”,`bootstrap` 只返回已有根类句柄。此过程不依赖数据库、全局命名空间、网络或异步运行时。 +`EndorObject` 保存 `ObjectHeader`、`Box`、托管属性和载荷引用快照。对象头仅保存身份、总强引用数和宿主外部强引用数。操作通过 Rust trait 的分发机制选择,不需要 Python type 系统;kind_name 只是调试标签,as_any 仅供原生 Rust 扩展识别自身的数据结构。 -```text -堆条目:Object { header: ObjectHeader { class }, data: ObjectData } +### 分配与句柄 -对象 对象头中的类型 类载荷中的父类 -Object Type 无 -Type Type Object -用户类 C Type 创建时指定的父类 -C 的实例 C 不适用 -``` +堆槽位为 `Option>`。Box 保持对象地址稳定,槽位向量扩容不移动实际对象。`ObjectRef` 由私有 VM 身份、索引、代数组成;每次访问先检查 VM,再检查范围、代数和对象是否存活。释放后槽位代数递增,耗尽后永久退役,不回绕。VM 身份由不回绕的原子计数器分配。无 unsafe、裸指针访问或外部句柄构造器。 -`create_class(name, parent)` 校验已有父类后创建不可变描述,并在公共对象头中设置类型为 Type。父类不可事后改写,不能形成继承环;只有内核启动路径可以创建没有父类的类。精确名称 Object 和 Type 保留。`create_instance` 校验类句柄,再创建以该类为类型的对象头和独立字段表。当前禁止直接实例化 Type 或继承 Type,返回 `UnsupportedMetaclass` 且不分配对象,避免把类工厂与普通实例工厂混用;尚未实现自定义元类、类型调用、描述符、方法与继承属性查找。 +### 引用所有权 -句柄结构是私有 VM 身份加 `usize` 槽位。VM 身份用原子计数器分配,耗尽时返回错误,不回绕复用。`Vec` 扩容会移动 Rust 对象,但外部不保存它们的地址,旧句柄继续通过索引访问。当前绝不删除槽位,所以无代际号。VM 不实现 Clone,复制句柄不能复制 VM 身份。 +allocate/new_object 返回一个外部强引用。ObjectRef 是可复制的借用句柄;复制或 Value::clone 不 retain。宿主通过 retain/release 显式持有与释放。头部总计数等于外部计数加来自托管属性及载荷引用快照的入边数。 -检查顺序是归属、范围、种类。外部没有从整数构造句柄的 API;低层无效槽位与伪造类型用私有单元测试覆盖。通用句柄可通过 `as_class/as_instance` 做校验后的转换。不同 VM 的引用、已经销毁 VM 的引用,在新 VM 中都报 `ForeignHandle`。同一 VM 中不在有效槽位的引用报 `InvalidHandle`。写字段前检查目标和引用值,失败不会覆盖原字段。 +属性写入先验证目标、被引用对象和计数溢出,再增加新引用、替换属性、释放旧引用;同目标替换不会短暂释放对象。属性读取和原生调用返回借用值,宿主需要跨越所有者生命周期使用时应先 retain。没有外部引用时,release 返回 NoOwnedReference,不能误释放内部属性引用。release 不代表删除任意对象。 -`Value` 中标量和字符串不强制堆分配,引用目前仅指类或实例。当前字符串值是按 Rust 所有权复制的文本,并非具有对象身份的字符串实例。`Float` 使用 Rust f64/PartialEq 行为,尚未作为最终语言语义承诺。统一对象头适用于堆对象,不表示所有标量都已对象化,也不复制 CPython 的引用计数或内存布局。Type 的自身类型引用是对象图中的环,继承关系仍然无环。 +载荷通过 trace 报告自己持有的引用;分配时验证全部边并记录快照,相同引用出现两次就计数两次。分配失败不改变已存在对象的计数。载荷持有的边在分配后必须保持不变,可变引用通过 VM 托管属性修改。扩展未声明的借用句柄不受 GC 保护,访问或返回时仍需通过 VM 验证。 -没有 GC,循环引用不形成 Rust 引用计数环,因为边只是句柄;所有内存随 VM 释放。长期运行时分配只增不减,尚无资源限额或内存不足的可恢复错误处理。宿主必须控制使用范围。 +### 清理与循环回收 + +总计数变为零时先摘除槽位,再用显式工作列表递减出边,最后 Drop 对象及原生资源;长链不会递归调用对象析构。对象不会因为仍有一个复制出的借用句柄而继续存活。 + +collect_cycles 是显式、全堆的追踪收集器:以外部引用数大于零的对象为根,沿属性和载荷快照标记,先批量摘除全部不可达对象,再扣减指向幸存对象的引用,最后 Drop 垃圾。这样可以处理自环、互相引用和原生载荷与属性构成的环,也不会误收仍可达的对象。trace API 遍历的是受管理引用,不是语言继承关系。 + +VM 销毁时 Box 的 Rust 所有权会清理全部剩余载荷,即使其计数仍大于零或尚未显式回收循环。没有语言级 __del__、复活、自动阈值、分代或增量收集。外部句柄不延长 VM 的生命周期。 + +### 操作协议 + +ObjectPayload 定义 repr、str、is_true、hash、compare、call 及属性许可。默认表示带诊断名和身份,默认 str 复用 repr,默认真值为 true,默认哈希基于身份,默认 Eq/Ne 按身份,排序和调用默认报 UnsupportedOperation。compare 返回 None 时尝试右侧反向操作,再使用身份回退;没有 Python 继承优先级规则。 + +Record 支持托管属性,NativeFunction 包装 Rust 函数指针。原生调用先验证参数句柄,再执行回调,再验证结果句柄。回调只计算返回值,不能修改或分配当前 VM;对象结果是借用句柄。自定义载荷可覆盖操作,若提供内容相等语义,必须保持哈希一致,或显式拒绝哈希。错误通过 Result 返回,不是语言异常机制。 + +Value 中标量和字符串不强制堆分配。Float 暂用 Rust f64 语义;完整字符串、数值对象和语言类型规则尚未定义。更详细的代码说明见 endor-object.md。 ## 宿主与持久化 @@ -34,12 +42,12 @@ SQLite 文件模式自动创建父目录、开启 WAL,连接池最多 5 条连 PostgreSQL 最多 5 条连接,获取/建立连接超时 5 秒,启动真实连接而非惰性连接。其配置失败或初始化失败均上抛,不进入 SQLite 分支。SQLite 的缺省只发生在**未配置** DATABASE_URL 时。 -元数据仅供宿主管理,不代表类、实例、句柄或整堆快照。重启宿主会创建新的 VM、新的 Object/Type 和新句柄,只保留文件/数据库内的元数据。服务仅写入 `host.version`,不会把句柄调试值写入数据库。 +元数据仅供宿主管理,不代表类、实例、句柄或整堆快照。重启宿主会创建新的空 VM;后续对象获得新身份和新句柄,只保留文件/数据库内的元数据。服务仅写入 `host.version`,不会把句柄调试值写入数据库。 -`aster-server` 保存只读 VM 和共享 Storage。GET /bootstrap 只读根类;GET /health 探测数据库可用性。没有对象操作 API、语言执行 API 或鉴权协议。HTTP/Tokio/SQLx 都不进入内核。后续浏览器宿主应通过明确的能力接口桥接,不能把浏览器类型塞进核心对象工厂。 +`aster-server` 保存只读 VM 和共享 Storage。GET /bootstrap 只读运行时标识和对象数量;GET /health 探测数据库可用性。没有对象操作 API、语言执行 API 或鉴权协议。HTTP/Tokio/SQLx 都不进入内核。后续浏览器宿主应通过明确的能力接口桥接,不能把浏览器类型塞进核心对象工厂。 ## 已决定与未决定 -已决定:唯一无父类 Object、基础元类 Type、堆对象统一对象头、类与实例使用不同载荷、类型与父类分开、句柄由 VM 校验、存储属于宿主。 +已决定:EndorObject 实现层基础结构、稳定 Box 分配、代际句柄、外部/内部引用计数、手动全堆循环回收、Rust 操作分发、存储属于宿主。 -未决定:自定义元类、字节码格式、调用栈、方法解析、语言异常、外部对象协议、线程间共享 VM、GC/根集、堆序列化、浏览器通信、完整字符串对象/数值对象语义。不得在后续交接中把这些当成已实现契约。 +未决定:语言根类、Python type/元类体系、继承、字节码格式、调用栈、可变 VM 回调上下文、语言异常与终结器、外部对象协议、共享可变 VM、自动/分代 GC、堆序列化和浏览器通信。不得在交接中把这些当成已实现契约。 diff --git a/docs/development.md b/docs/development.md index 2461237..7cfcc25 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,21 +6,25 @@ 核心 API 示例在 `aster-core/src/lib.rs`,由 rustdoc 实际编译执行。公开语义测试在 `aster-core/tests/runtime.rs`;伪造句柄仅在私有堆单元测试中构造,禁止为测试添加公开非法构造器。 -堆对象通过 `ObjectHeader` 统一保存类型,`Class`/`Instance` 仅为不同载荷。旧的 `vm.instance(handle)?.class()` 改为 `vm.type_of(handle.into())`,类对象也使用同一接口。启动分配两个基础对象:Object 与 Type;CLI 示例另建一个类和两个实例后共 5 个对象,HTTP `/bootstrap` 的初始对象数为 2。新增测试覆盖 Type 自身类型、类型与父类分离、跨 VM 类型查询、扩容后类型引用稳定,以及拒绝通过普通工厂创建自定义元类。 +用户已澄清:当前实现 Endor 的 VM 运行时,不实现 Python type 系统,也不定义语言根类。旧的 Class/Type、create_class、create_instance、root_class 和 type_of API 已移除。公共结构和操作接口在 object.rs,分配、引用计数和循环回收在 heap.rs,对外检查入口在 vm.rs。详见 endor-object.md。 + +关键所有权规则:分配给一个外部强引用;句柄拷贝与 Value 克隆只产生借用;额外持有使用 retain。托管属性和 trace 声明的不可变载荷边分别计数。get_attr/call 返回借用值。长链释放使用迭代队列;循环回收以外部引用为根。添加可变载荷引用时必须设计 VM 管理的更新协议,不能只改 trace 的返回结果。 + +CLI bootstrap 演示两个原生运行时对象并显式释放;HTTP /bootstrap 返回 object_model=EndorObject、type_system=false、heap_objects=0。修改宿主表示时同步 scripts/smoke.py。 存储测试用 `tempfile` 隔离文件,关闭池后重开,验证独立连接生命周期中的持久化。内存测试验证克隆共享、独立 store 隔离、特殊字符参数绑定、更新/缺失键。PostgreSQL 失败测试用只监听但不处理协议的本地 TCP socket,检验真实连接超时和不降级;成功测试显式 ignore,需要用户提供专用测试库。 -尚未运行的冒烟脚本用于跨进程调用 CLI、启动 HTTP 宿主、探测真实 TCP 端点、发送 SIGTERM 并确认正常退出。它只创建临时目录,不改 `.env`,不操作外部服务器。端口通过临时绑定选择,释放后启动间存在很小的端口竞争窗口;在指定验证环境中若冲突,可重试整个脚本。 +远端冒烟脚本用于跨进程调用 CLI、启动 HTTP 宿主、探测真实 TCP 端点、发送 SIGTERM 并确认正常退出。它只创建临时目录,不改 `.env`,不操作外部服务器。端口通过临时绑定选择,释放后启动间存在很小的端口竞争窗口;在指定验证环境中若冲突,可重试整个脚本。 依赖使用 Cargo.lock。此前编译器为 1.91.1;更换依赖后须在指定环境检查 MSRV 和容器构建版本。核心测试无需数据库,但 Cargo 首次依赖解析仍可能访问注册表;不要因此在本机恢复下载或测试。 ## 下一阶段建议 -1. 先约定对象字段与方法查找规则、宿主调用的参数/返回值和错误边界,补语义测试。 -2. 在 core 内构建最小、可测的执行循环,输入先用手写指令,继续不做解析器和渲染器。 -3. 根据执行栈、对象图、宿主持有引用确定 GC 根集;如需回收槽位,升级代际/间接句柄模型并补陈旧句柄测试。 -4. 对外部对象、浏览器能力、配额与取消建立最小接口,以实际需求驱动,不预先堆砌空 trait。 -5. 需要升级元数据 schema 时引入可追踪的迁移;若要求 VM 快照,另行定义持久身份与格式,不能直接序列化现有句柄。 +1. 为执行栈与宿主持有值建立明确的强引用根管理,扩展原生调用上下文,补对应生命周期测试。 +2. 在 core 内构建可测的最小执行循环,先用手写指令,继续不做解析器和渲染器。 +3. 按实际分配负载设计 GC 触发、资源限额与暂停策略;现有手动循环回收不等于分代或增量 GC。 +4. 浏览器能力与外部对象桥接使用原生载荷;保持 core 无网络或宿主库依赖。 +5. VM 快照需要另行定义持久身份与格式,不能序列化当前进程句柄。语言类型、根类和继承以后再确定。 ## Git 状态与后续远程 diff --git a/docs/endor-object.md b/docs/endor-object.md new file mode 100644 index 0000000..9cc1db2 --- /dev/null +++ b/docs/endor-object.md @@ -0,0 +1,110 @@ +# EndorObject 代码说明 + +当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。这里没有语言根类、元类、继承或 Python type 系统。标量仍然可以直接保存在 Value 中,不要求一切装箱。 + +## 代码分布 + +| 文件 | 职责 | +| --- | --- | +| `crates/aster-core/src/object.rs` | EndorObject、对象头、Value、操作接口和内置原生载荷 | +| `crates/aster-core/src/heap.rs` | 稳定分配、句柄验证、引用计数、槽位复用、循环回收 | +| `crates/aster-core/src/vm.rs` | 对外 API、错误处理和操作分发 | +| `crates/aster-core/tests/runtime.rs` | 宿主可观察的对象行为和生命周期检查 | + +## EndorObject 保存什么 + +```text +EndorObject + header 身份、总强引用数、外部强引用数 + payload 原生 Rust 数据及操作接口 + attributes VM 管理的可变属性表 + payload_edges 分配时记录的载荷引用快照 +``` + +`ObjectHeader` 不保存语言类型指针。`identity` 是 ObjectRef,用于识别同一个对象。`strong_refs` 是所有强引用的总数,`external_refs` 是宿主持有数,也是循环回收的根来源。 + +`payload` 是 `Box`。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;不需要创建一个 Python 式的 Type 对象。`kind_name` 只用于诊断,`as_any` 只用于原生 Rust 扩展判断自己能否处理另一份载荷。 + +`attributes` 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。 + +`payload_edges` 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。分配后这些载荷边必须保持不变;需要修改的对象关系放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。 + +## 指针与句柄 + +Heap 使用 `Option>` 保存槽位,实际对象位于独立分配的 Box 中。槽位向量扩容时,移动的是 Box,存活对象本身的地址保持稳定。 + +公开的 ObjectRef 使用三部分定位: + +```text +heap_id 属于哪个 VM +index 位于哪个槽位 +generation 这是该槽位的哪一次分配 +``` + +每次访问检查归属、范围、代数和存活状态。对象释放后,复用同一槽位会更新代数,所以旧句柄不会指向后来分配的新对象。代数耗尽就停用该槽位,VM 身份也不回绕。 + +公共 API 不暴露裸指针解引用,也不允许调用方构造索引。句柄是进程内引用,不是数据库持久 ID。 + +## 分配、持有与释放 + +`new_object()` 创建 Record;`allocate(payload)` 创建指定原生载荷的对象。分配成功时,总引用数和外部引用数均为 1,返回这一份外部持有对应的句柄。 + +```rust +let object = vm.new_object()?; +let borrowed = object; // 只有句柄复制,引用数仍为 1。 +vm.retain(borrowed)?; // 再持有一份,总数和外部数变为 2。 +vm.release(object)?; // 释放一份,仍然存活。 +vm.release(borrowed)?; // 最后一份释放,清理对象。 +``` + +ObjectRef 是 Copy,但它不是自动管理持有关系的智能指针。宿主有责任为每一份外部持有配对 retain/release。没有外部持有时不能通过 release 消耗属性拥有的内部引用;此时返回 NoOwnedReference。 + +引用数归零后,堆先摘除对象,再迭代减少它持有的出边,最后执行 Rust Drop。若某个目标也归零,就加入工作列表继续处理。长对象链不会形成递归析构栈。载荷可实现 Drop 释放原生文件、缓冲区或其他宿主资源;这里没有语言级终结器和复活协议。 + +销毁整个 Vm 时,Rust 所有权会释放全部剩余对象,包括尚未显式回收的环。复制出的句柄不能让 VM 本身继续存活。 + +## 默认操作和覆盖方式 + +ObjectPayload 提供以下默认行为,载荷可以逐项覆盖: + +| 操作 | 默认行为 | +| --- | --- | +| `repr` | 诊断名和对象身份组成的文本 | +| `str` | 调用该载荷的 repr | +| `is_true` | true | +| `hash` | 基于对象身份计算哈希 | +| `compare` | 返回未实现,由 VM 尝试反向比较;Eq/Ne 最终按身份回退,排序报错 | +| `call` | 返回不支持调用的错误 | +| `allows_attributes` | 不允许属性,载荷可显式允许 | +| `trace` | 没有载荷持有的对象引用 | + +Record 使用身份语义并开启属性。NativeFunction 保存名称和 Rust 函数指针,覆盖 repr 和 call。原生函数收到 Value 参数并返回 Result;VM 在调用前后检查参数和返回值中的对象句柄。 + +原生调用当前不能修改 VM 或直接在该 VM 中分配新对象;对象参数和对象返回值是借用句柄。这个接口足够表达标量计算和已有对象返回,还没有语言执行栈、函数帧、闭包或字节码执行。 + +compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。它不实现 Python 的子类型优先规则。内容比较由扩展定义;如果两个对象内容相等,它们必须给出相同哈希,或者明确拒绝哈希。 + +## 属性与错误原子性 + +`get_attr` 读取借用值;`set_attr` 写入;`del_attr` 删除。对象属性持有内部强引用,删除或覆盖时会释放旧引用。宿主若希望读取到的对象在属性删除后继续存活,必须先 retain。 + +写入先验证所有句柄和计数溢出,再增加新引用,最后替换并释放旧引用。因此跨 VM、失效句柄或溢出错误不会覆盖原值;把同一个对象重复赋给同一属性,也不会在替换间隙误释放它。 + +## 循环回收 + +引用计数无法自行释放 A → B → A,所以提供显式 `collect_cycles()`: + +1. 把 external_refs 大于零的对象作为根。 +2. 沿托管属性和载荷边标记所有可达对象。 +3. 先从槽位中摘除全部不可达对象。 +4. 扣减垃圾指向幸存对象的引用,再 Drop 全部垃圾。 + +该过程会保留可达的环,回收无人持有的自环、互引用环,以及原生载荷与托管属性混合构成的环。收集返回回收数量,未自动按阈值触发。 + +## 当前边界 + +已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。 + +尚未实现:Python type 系统、语言根类、继承和描述符、语言执行器、可变 VM 调用上下文、自动/分代/增量 GC、语言终结器/复活、资源限额和 VM 快照。数据库仍然只保存宿主元数据。 + +实际测试结果以本次提交对应的远端 Gitea CI 为准,不在本地构建、测试或下载依赖。 diff --git a/docs/validation.md b/docs/validation.md index 06b8484..cfe3961 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -54,3 +54,10 @@ 本次把堆条目改为 `ObjectHeader` 加载荷,所有类与实例通过同一对象头引用类型;启动同时建立 Object 和 Type。新增或更新类型引导、类型查询、无效句柄、跨 VM 隔离、工厂边界、扩容稳定性及文档示例的检查,并同步远端 CLI/HTTP 冒烟的对象数预期。 本次未在本地运行 Rust 构建、测试、格式化脚本或冒烟脚本,也未下载依赖。本记录提交时尚未取得新版本远端结果,实际通过与否以本次提交对应的 Gitea CI 记录为准;之前已成功的 #160/#161 仅验证旧版对象模型,不代表本次修改已通过。 + + +## EndorObject 运行时边界修正(2026-10-18) + +用户明确当前目标是实现层公共对象,不需要 Python type 系统。移除语言 Object/Type 引导与类工厂,新增 EndorObject 原生操作、稳定 Box、代际槽位、外部/内部引用计数、迭代释放及显式循环回收;CLI/HTTP 冒烟改为运行时对象演示与空堆信息。 + +本次针对身份与地址稳定、持有与借用、属性替换与删除、错误写入原子性、长链释放、可达环保护、不可达环清理、载荷边计数、反向比较、原生调用错误和 VM 销毁资源释放编写检查。提交时未取得本版远端结果,以对应 Gitea CI 为准。没有在本地运行构建、测试、冒烟,也没有下载依赖。旧版 #162 已成功,但不代表本次修正通过。 diff --git a/scripts/smoke.py b/scripts/smoke.py index 53ac187..cd800b7 100644 --- a/scripts/smoke.py +++ b/scripts/smoke.py @@ -31,8 +31,9 @@ def main(): # This must succeed even when an explicit database URL is invalid. isolated_env = {**env, "DATABASE_URL": "invalid-database-url"} bootstrap = run(cli, "bootstrap", env=isolated_env) - assert "root=Object parent=None" in bootstrap, bootstrap - assert "instances_distinct=true heap_objects=5" in bootstrap, bootstrap + assert "runtime=EndorObject type_system=disabled" in bootstrap, bootstrap + assert "objects_distinct=true heap_objects=2" in bootstrap, bootstrap + assert "released heap_objects=0" in bootstrap, bootstrap assert not Path(env["SQLITE_PATH"]).exists(), "bootstrap must not open storage" assert "sqlite" in run(cli, "metadata", "put", "smoke", "持久化成功", env=env) assert run(cli, "metadata", "get", "smoke", env=env) == "持久化成功" @@ -61,7 +62,7 @@ def main(): for _ in range(2): with urllib.request.urlopen(f"http://127.0.0.1:{port}/bootstrap", timeout=2) as response: info = json.load(response) - assert info == {"root_class": "Object", "root_has_parent": False, "heap_objects": 2}, info + assert info == {"object_model": "EndorObject", "type_system": False, "heap_objects": 0}, info finally: process.send_signal(signal.SIGTERM) try: