From d394f07fc2f6bab9841bfd7df912041e202d36c6 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 18 Oct 2026 13:38:00 +0800 Subject: [PATCH] Unify heap objects with typed headers and bootstrap Type --- README.md | 14 +++-- crates/aster-core/src/heap.rs | 88 +++++++++++++++++++++++++++--- crates/aster-core/src/lib.rs | 7 ++- crates/aster-core/src/vm.rs | 78 ++++++++++++++++---------- crates/aster-core/tests/runtime.rs | 72 ++++++++++++++++++++---- docs/architecture.md | 24 ++++++-- docs/development.md | 2 + docs/validation.md | 6 ++ scripts/smoke.py | 6 +- 9 files changed, 235 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index c041d1e..4bc987a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Aster VM -Rust 编写的可嵌入语言运行时,当前为第一阶段:启动时引导根类 `Object`,并由对象工厂创建类和实例。这里的“顶级类”指根类;“生成器”指运行时引导与对象工厂。 +Rust 编写的可嵌入语言运行时,当前为第一阶段:启动时引导根类 `Object` 和基础元类 `Type`,并由对象工厂创建类和实例。所有堆对象都有公共对象头和类型句柄,类本身也是语言对象。这里的“顶级类”指根类;“生成器”指运行时引导与对象工厂。 本项目尚无词法/语法前端、字节码执行器、渲染器、GC 或完整元类体系,不承诺 Python 兼容。数据库只保存宿主元数据,不保存或恢复 VM 堆。 @@ -24,7 +24,7 @@ cargo build --workspace --locked cargo run -p aster-cli -- bootstrap ``` -预期输出含 `root=Object parent=None`、`instances_distinct=true heap_objects=4`。这条命令不读取数据库配置、不打开数据库;即使设置了错误的 `DATABASE_URL` 也不影响引导。 +预期输出含 `root=Object parent=None`、`instances_distinct=true heap_objects=5`(Object、Type、Example 与两个实例)。这条命令不读取数据库配置、不打开数据库;即使设置了错误的 `DATABASE_URL` 也不影响引导。 ```rust use aster_core::{Value, Vm}; @@ -34,7 +34,9 @@ fn main() -> Result<(), aster_core::VmError> { 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.instance(instance)?.class(), point); + 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()); Ok(()) } ``` @@ -65,7 +67,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`、无父类、堆对象数 1,多次读取不会重复引导。支持 Ctrl-C/Unix SIGTERM 优雅退出。 +`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 优雅退出。 这两个端点不提供执行语言、对象写入或业务 API。服务没有认证层;示例 Compose 端口仅绑定宿主回环地址,外部访问由后续宿主部署决定。 @@ -156,7 +158,9 @@ docker stop aster-pg-test ## 当前语义与后续开发 -- `Object` 是每个 VM 启动时唯一分配的根类,没有父类。类描述与类对象合为一个堆对象;实例保存类句柄。普通类必须指定已有父类,名称只是标签,无全局命名空间;保留精确名称 `Object`,其他同名类允许存在。 +- 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` 持有,类和实例使用堆句柄。尚未定义完整语言数值、方法查找、继承执行或异常语义。 diff --git a/crates/aster-core/src/heap.rs b/crates/aster-core/src/heap.rs index 1607363..f841f6e 100644 --- a/crates/aster-core/src/heap.rs +++ b/crates/aster-core/src/heap.rs @@ -42,8 +42,8 @@ pub enum Value { Object(ObjectRef), } -/// The class object and its immutable description are one heap allocation. -/// There is deliberately no metaclass or implicit class-of-class relationship. +/// 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, @@ -62,26 +62,59 @@ impl Class { #[derive(Debug)] pub struct Instance { - pub(crate) class: ClassRef, pub(crate) fields: BTreeMap, } impl Instance { - pub fn class(&self) -> ClassRef { - self.class - } - 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 enum Object { +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 { @@ -90,6 +123,35 @@ pub(crate) struct Heap { } 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)) @@ -165,6 +227,16 @@ mod tests { )); } + #[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, + }; + assert_eq!(vm.type_of(invalid), Err(VmError::InvalidHandle)); + } + #[test] fn invalid_value_reference_cannot_replace_a_field() { let mut vm = crate::Vm::new().unwrap(); diff --git a/crates/aster-core/src/lib.rs b/crates/aster-core/src/lib.rs index d51ee3a..b2cc9ad 100644 --- a/crates/aster-core/src/lib.rs +++ b/crates/aster-core/src/lib.rs @@ -6,12 +6,15 @@ //! 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.instance(instance)?.class(), point); +//! assert_eq!(vm.type_of(instance.into())?, point); +//! assert_eq!(vm.type_of(point.into())?, vm.type_class()); //! # Ok::<(), aster_core::VmError>(()) //! ``` mod heap; mod vm; -pub use heap::{Class, ClassRef, Instance, InstanceRef, Object, ObjectRef, Value}; +pub use heap::{ + Class, ClassRef, Instance, InstanceRef, Object, ObjectData, ObjectHeader, ObjectRef, Value, +}; pub use vm::{Vm, VmError}; diff --git a/crates/aster-core/src/vm.rs b/crates/aster-core/src/vm.rs index c984f48..145261f 100644 --- a/crates/aster-core/src/vm.rs +++ b/crates/aster-core/src/vm.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::fmt; use crate::heap::Heap; -use crate::{Class, ClassRef, Instance, InstanceRef, Object, ObjectRef, Value}; +use crate::{Class, ClassRef, Instance, InstanceRef, Object, ObjectData, ObjectRef, Value}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VmError { @@ -12,6 +12,7 @@ pub enum VmError { ExpectedInstance, EmptyClassName, ReservedClassName, + UnsupportedMetaclass, IdentityExhausted, } @@ -23,7 +24,8 @@ impl fmt::Display for VmError { Self::ExpectedClass => "object is not a class", Self::ExpectedInstance => "object is not an instance", Self::EmptyClassName => "class name cannot be blank", - Self::ReservedClassName => "Object is reserved for the root class", + 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", }) } @@ -31,22 +33,24 @@ impl fmt::Display for VmError { impl std::error::Error for VmError {} -/// Owns its heap and exactly one root class. Not Clone: handles have one owner. +/// Owns its heap, the root Object class and its Type metaclass. +/// Not Clone: handles have one owner. #[derive(Debug)] pub struct Vm { heap: Heap, root: ClassRef, + type_class: ClassRef, } impl Vm { - /// Bootstraps Object before returning. Does not perform any host I/O. + /// Bootstraps Object and Type before returning. Does not perform host I/O. pub fn new() -> Result { - let mut heap = Heap::new()?; - let root = ClassRef(heap.allocate(Object::Class(Class { - name: "Object".into(), - parent: None, - }))); - Ok(Self { heap, root }) + let (heap, root, type_class) = Heap::bootstrap()?; + Ok(Self { + heap, + root, + type_class, + }) } /// Idempotent: construction has already performed bootstrap exactly once. @@ -58,6 +62,10 @@ impl Vm { self.root } + pub fn type_class(&self) -> ClassRef { + self.type_class + } + pub fn object_count(&self) -> usize { self.heap.len() } @@ -70,26 +78,35 @@ impl Vm { 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" { + if name == "Object" || name == "Type" { return Err(VmError::ReservedClassName); } - Ok(ClassRef(self.heap.allocate(Object::Class(Class { - name, - parent: Some(parent), - })))) + Ok(ClassRef(self.heap.allocate(Object::new( + self.type_class, + ObjectData::Class(Class { + name, + parent: Some(parent), + }), + )))) } pub fn create_instance(&mut self, class: ClassRef) -> Result { self.class(class)?; - Ok(InstanceRef(self.heap.allocate(Object::Instance( - Instance { - class, + if class == self.type_class { + return Err(VmError::UnsupportedMetaclass); + } + Ok(InstanceRef(self.heap.allocate(Object::new( + class, + ObjectData::Instance(Instance { fields: BTreeMap::new(), - }, + }), )))) } @@ -97,17 +114,22 @@ impl Vm { 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 class(&self, reference: ClassRef) -> Result<&Class, VmError> { - match self.heap.get(reference.0)? { - Object::Class(class) => Ok(class), - Object::Instance(_) => Err(VmError::ExpectedClass), + match self.heap.get(reference.0)?.data() { + ObjectData::Class(class) => Ok(class), + ObjectData::Instance(_) => Err(VmError::ExpectedClass), } } pub fn instance(&self, reference: InstanceRef) -> Result<&Instance, VmError> { - match self.heap.get(reference.0)? { - Object::Instance(instance) => Ok(instance), - Object::Class(_) => Err(VmError::ExpectedInstance), + match self.heap.get(reference.0)?.data() { + ObjectData::Instance(instance) => Ok(instance), + ObjectData::Class(_) => Err(VmError::ExpectedInstance), } } @@ -135,12 +157,12 @@ impl Vm { if let Value::Object(reference) = &value { self.heap.get(*reference)?; } - match self.heap.get_mut(instance.0)? { - Object::Instance(instance) => { + match &mut self.heap.get_mut(instance.0)?.data { + ObjectData::Instance(instance) => { instance.fields.insert(name.into(), value); Ok(()) } - Object::Class(_) => Err(VmError::ExpectedInstance), + ObjectData::Class(_) => Err(VmError::ExpectedInstance), } } } diff --git a/crates/aster-core/tests/runtime.rs b/crates/aster-core/tests/runtime.rs index de353d4..7425262 100644 --- a/crates/aster-core/tests/runtime.rs +++ b/crates/aster-core/tests/runtime.rs @@ -1,4 +1,4 @@ -use aster_core::{Value, Vm, VmError}; +use aster_core::{ObjectData, Value, Vm, VmError}; #[test] fn bootstrap_is_unique_and_root_has_no_parent() { @@ -6,11 +6,30 @@ fn bootstrap_is_unique_and_root_has_no_parent() { let root = vm.root_class(); assert_eq!(vm.bootstrap(), root); assert_eq!(vm.bootstrap(), root); - assert_eq!(vm.object_count(), 1); + assert_eq!(vm.object_count(), 2); assert_eq!(vm.class(root).unwrap().name(), "Object"); assert_eq!(vm.class(root).unwrap().parent(), None); } +#[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)); + } +} + #[test] fn factories_preserve_class_links_and_distinct_identity() { let mut vm = Vm::new().unwrap(); @@ -22,8 +41,13 @@ fn factories_preserve_class_links_and_distinct_identity() { let plain = vm.create_instance(root).unwrap(); assert_ne!(first, second); assert_eq!(vm.class(child).unwrap().parent(), Some(class)); - assert_eq!(vm.instance(first).unwrap().class(), class); - assert_eq!(vm.instance(plain).unwrap().class(), root); + 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(); @@ -58,6 +82,13 @@ fn foreign_handles_are_rejected_without_mutation() { 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) @@ -92,16 +123,35 @@ fn invalid_names_do_not_allocate_and_names_are_not_a_namespace() { vm.create_class(" ", vm.root_class()), Err(VmError::EmptyClassName) ); - assert_eq!( - vm.create_class("Object", vm.root_class()), - Err(VmError::ReservedClassName) - ); - assert_eq!(vm.object_count(), 1); + 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); } +#[test] +fn type_cannot_be_used_to_bypass_the_class_factory() { + 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(); @@ -111,7 +161,9 @@ fn handles_remain_valid_when_the_arena_grows() { for _ in 0..4096 { vm.create_instance(root).unwrap(); } - assert_eq!(vm.instance(first).unwrap().class(), root); + 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)) diff --git a/docs/architecture.md b/docs/architecture.md index 8bf29a9..e6bc02c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,15 +2,27 @@ ## 内核 -`Vm` 拥有私有 `Heap`。`Vm::new` 分配一个 `Object::Class(Class)`,其描述为名称 Object、父类 None,然后保存 `ClassRef`。不存在可观察的“半初始化 VM”;`bootstrap` 只返回已建立的根类句柄。因此根类不依赖数据库、全局命名空间、元类、网络或异步运行时。 +`Vm` 拥有私有 `Heap`。堆中每个条目都是统一的 Rust `Object`:公共 `ObjectHeader` 保存类型句柄,`ObjectData` 保存类元数据或实例字段。`Class` 只是类载荷,不是独立于公共对象头的语言对象。实例不再重复保存类型,统一通过 `vm.type_of(ObjectRef)` 查询对象头;`vm.class`/`vm.instance` 读取相应载荷。 -`create_class(name, parent)` 校验已有父类后创建不可变描述,父类不可事后改写,不能形成继承环;只有内核启动路径可以创建没有父类的类。`create_instance` 校验类句柄再创建独立字段表。没有复制原型或隐式字段继承。未实现方法与实例字段查找的语言规则。 +`Vm::new` 通过私有 `Heap::bootstrap` 一次性建立 Object 和 Type。Object 无父类,类型为 Type;Type 的父类为 Object,类型为自身。初始化器在独占的空堆中计算两个槽位的私有句柄并填入完整对象,不使用裸指针或可空的临时类型引用;完整堆建立后才返回。不存在可观察的“半初始化 VM”,`bootstrap` 只返回已有根类句柄。此过程不依赖数据库、全局命名空间、网络或异步运行时。 + +```text +堆条目:Object { header: ObjectHeader { class }, data: ObjectData } + +对象 对象头中的类型 类载荷中的父类 +Object Type 无 +Type Type Object +用户类 C Type 创建时指定的父类 +C 的实例 C 不适用 +``` + +`create_class(name, parent)` 校验已有父类后创建不可变描述,并在公共对象头中设置类型为 Type。父类不可事后改写,不能形成继承环;只有内核启动路径可以创建没有父类的类。精确名称 Object 和 Type 保留。`create_instance` 校验类句柄,再创建以该类为类型的对象头和独立字段表。当前禁止直接实例化 Type 或继承 Type,返回 `UnsupportedMetaclass` 且不分配对象,避免把类工厂与普通实例工厂混用;尚未实现自定义元类、类型调用、描述符、方法与继承属性查找。 句柄结构是私有 VM 身份加 `usize` 槽位。VM 身份用原子计数器分配,耗尽时返回错误,不回绕复用。`Vec` 扩容会移动 Rust 对象,但外部不保存它们的地址,旧句柄继续通过索引访问。当前绝不删除槽位,所以无代际号。VM 不实现 Clone,复制句柄不能复制 VM 身份。 检查顺序是归属、范围、种类。外部没有从整数构造句柄的 API;低层无效槽位与伪造类型用私有单元测试覆盖。通用句柄可通过 `as_class/as_instance` 做校验后的转换。不同 VM 的引用、已经销毁 VM 的引用,在新 VM 中都报 `ForeignHandle`。同一 VM 中不在有效槽位的引用报 `InvalidHandle`。写字段前检查目标和引用值,失败不会覆盖原字段。 -`Value` 中标量和字符串不强制堆分配,引用目前仅指类或实例。当前字符串值是按 Rust 所有权复制的文本,并非具有对象身份的字符串实例。`Float` 使用 Rust f64/PartialEq 行为,尚未作为最终语言语义承诺。类不是自身或其他类的实例,根类也没有隐式元类。 +`Value` 中标量和字符串不强制堆分配,引用目前仅指类或实例。当前字符串值是按 Rust 所有权复制的文本,并非具有对象身份的字符串实例。`Float` 使用 Rust f64/PartialEq 行为,尚未作为最终语言语义承诺。统一对象头适用于堆对象,不表示所有标量都已对象化,也不复制 CPython 的引用计数或内存布局。Type 的自身类型引用是对象图中的环,继承关系仍然无环。 没有 GC,循环引用不形成 Rust 引用计数环,因为边只是句柄;所有内存随 VM 释放。长期运行时分配只增不减,尚无资源限额或内存不足的可恢复错误处理。宿主必须控制使用范围。 @@ -22,12 +34,12 @@ SQLite 文件模式自动创建父目录、开启 WAL,连接池最多 5 条连 PostgreSQL 最多 5 条连接,获取/建立连接超时 5 秒,启动真实连接而非惰性连接。其配置失败或初始化失败均上抛,不进入 SQLite 分支。SQLite 的缺省只发生在**未配置** DATABASE_URL 时。 -元数据仅供宿主管理,不代表类、实例、句柄或整堆快照。重启宿主会创建新的 VM、新的 Object 和新句柄,只保留文件/数据库内的元数据。服务仅写入 `host.version`,不会把句柄调试值写入数据库。 +元数据仅供宿主管理,不代表类、实例、句柄或整堆快照。重启宿主会创建新的 VM、新的 Object/Type 和新句柄,只保留文件/数据库内的元数据。服务仅写入 `host.version`,不会把句柄调试值写入数据库。 `aster-server` 保存只读 VM 和共享 Storage。GET /bootstrap 只读根类;GET /health 探测数据库可用性。没有对象操作 API、语言执行 API 或鉴权协议。HTTP/Tokio/SQLx 都不进入内核。后续浏览器宿主应通过明确的能力接口桥接,不能把浏览器类型塞进核心对象工厂。 ## 已决定与未决定 -已决定:唯一无父类 Object、类和实例分离、实例引用类、句柄由 VM 校验、存储属于宿主。 +已决定:唯一无父类 Object、基础元类 Type、堆对象统一对象头、类与实例使用不同载荷、类型与父类分开、句柄由 VM 校验、存储属于宿主。 -未决定:字节码格式、调用栈、方法解析、语言异常、外部对象协议、线程间共享 VM、GC/根集、堆序列化、浏览器通信、完整字符串对象/数值对象语义。不得在后续交接中把这些当成已实现契约。 +未决定:自定义元类、字节码格式、调用栈、方法解析、语言异常、外部对象协议、线程间共享 VM、GC/根集、堆序列化、浏览器通信、完整字符串对象/数值对象语义。不得在后续交接中把这些当成已实现契约。 diff --git a/docs/development.md b/docs/development.md index 9220234..2461237 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,6 +6,8 @@ 核心 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 类型查询、扩容后类型引用稳定,以及拒绝通过普通工厂创建自定义元类。 + 存储测试用 `tempfile` 隔离文件,关闭池后重开,验证独立连接生命周期中的持久化。内存测试验证克隆共享、独立 store 隔离、特殊字符参数绑定、更新/缺失键。PostgreSQL 失败测试用只监听但不处理协议的本地 TCP socket,检验真实连接超时和不降级;成功测试显式 ignore,需要用户提供专用测试库。 尚未运行的冒烟脚本用于跨进程调用 CLI、启动 HTTP 宿主、探测真实 TCP 端点、发送 SIGTERM 并确认正常退出。它只创建临时目录,不改 `.env`,不操作外部服务器。端口通过临时绑定选择,释放后启动间存在很小的端口竞争窗口;在指定验证环境中若冲突,可重试整个脚本。 diff --git a/docs/validation.md b/docs/validation.md index 4add134..06b8484 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -48,3 +48,9 @@ 准备阶段只读检查:Docker 29.1.3、Compose 2.40.3、x86_64、约 1.7 GiB 内存/4 GiB swap、73 GiB 可用磁盘;没有主机 Rust 或 buildx,已缓存 PostgreSQL 17 镜像。选用单任务 Rust 构建、legacy Docker builder,默认生产端口 18081(服务器回环地址),PostgreSQL 数据目录独立。 此处不预先宣称远端 CI 或部署已成功;最终结果以 Gitea Actions 对应运行与服务器状态核验为准。 + +## 统一对象头变更(2026-10-18) + +本次把堆条目改为 `ObjectHeader` 加载荷,所有类与实例通过同一对象头引用类型;启动同时建立 Object 和 Type。新增或更新类型引导、类型查询、无效句柄、跨 VM 隔离、工厂边界、扩容稳定性及文档示例的检查,并同步远端 CLI/HTTP 冒烟的对象数预期。 + +本次未在本地运行 Rust 构建、测试、格式化脚本或冒烟脚本,也未下载依赖。本记录提交时尚未取得新版本远端结果,实际通过与否以本次提交对应的 Gitea CI 记录为准;之前已成功的 #160/#161 仅验证旧版对象模型,不代表本次修改已通过。 diff --git a/scripts/smoke.py b/scripts/smoke.py index e180900..53ac187 100644 --- a/scripts/smoke.py +++ b/scripts/smoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Local end-to-end smoke test, no Docker or external DB required.""" +"""Remote CI end-to-end smoke test, no external database required.""" import json import os from pathlib import Path @@ -32,7 +32,7 @@ def main(): 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=4" in bootstrap, bootstrap + assert "instances_distinct=true heap_objects=5" 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 +61,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": 1}, info + assert info == {"root_class": "Object", "root_has_parent": False, "heap_objects": 2}, info finally: process.send_signal(signal.SIGTERM) try: