Unify heap objects with typed headers and bootstrap Type
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 14m16s

This commit is contained in:
2026-10-18 13:38:00 +08:00
parent afcaf5cff8
commit d394f07fc2
9 changed files with 235 additions and 62 deletions
+9 -5
View File
@@ -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` 持有,类和实例使用堆句柄。尚未定义完整语言数值、方法查找、继承执行或异常语义。
+80 -8
View File
@@ -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<String, Value>,
}
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<Self, VmError> {
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();
+5 -2
View File
@@ -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};
+50 -28
View File
@@ -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<Self, VmError> {
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<ClassRef, VmError> {
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<InstanceRef, VmError> {
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<ClassRef, VmError> {
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),
}
}
}
+62 -10
View File
@@ -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))
+18 -6
View File
@@ -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/根集、堆序列化、浏览器通信、完整字符串对象/数值对象语义。不得在后续交接中把这些当成已实现契约。
+2
View File
@@ -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`,不操作外部服务器。端口通过临时绑定选择,释放后启动间存在很小的端口竞争窗口;在指定验证环境中若冲突,可重试整个脚本。
+6
View File
@@ -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 仅验证旧版对象模型,不代表本次修改已通过。
+3 -3
View File
@@ -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: