Implement EndorObject runtime operations and managed lifetimes without language types
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 14m4s
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 14m4s
This commit is contained in:
@@ -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<EndorObject>` 保持存活对象地址稳定;槽位复用更新代数,旧句柄不会指向新对象。外部不构造堆索引,也不操作裸指针。
|
||||
- 分配返回一个外部强引用;复制句柄或含句柄的 `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;确切解析版本以锁文件为准。
|
||||
|
||||
@@ -27,22 +27,22 @@ async fn run() -> Result<(), Box<dyn Error>> {
|
||||
{
|
||||
[] | ["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}"),
|
||||
|
||||
+284
-183
@@ -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<ClassRef> for ObjectRef {
|
||||
fn from(value: ClassRef) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InstanceRef> 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<ClassRef>,
|
||||
}
|
||||
|
||||
impl Class {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<ClassRef> {
|
||||
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<String, Value>,
|
||||
struct Slot {
|
||||
generation: u64,
|
||||
object: Option<Box<EndorObject>>,
|
||||
}
|
||||
|
||||
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<Object>,
|
||||
slots: Vec<Slot>,
|
||||
free: Vec<usize>,
|
||||
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<Self, VmError> {
|
||||
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<dyn ObjectPayload>,
|
||||
) -> Result<ObjectRef, VmError> {
|
||||
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<BTreeMap<ObjectRef, usize>, VmError> {
|
||||
let mut counts = BTreeMap::<ObjectRef, usize>::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<ObjectRef, usize>) {
|
||||
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<Value, VmError> {
|
||||
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<ObjectRef>) {
|
||||
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<EndorObject> {
|
||||
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<ObjectRef>) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<String, VmError> {
|
||||
Ok(format!("<{} {identity}>", self.kind_name()))
|
||||
}
|
||||
|
||||
fn str(&self, identity: ObjectRef) -> Result<String, VmError> {
|
||||
self.repr(identity)
|
||||
}
|
||||
|
||||
fn is_true(&self) -> Result<bool, VmError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn hash(&self, identity: ObjectRef) -> Result<u64, VmError> {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
identity.hash(&mut hasher);
|
||||
Ok(hasher.finish())
|
||||
}
|
||||
|
||||
/// None means "not implemented"; the VM tries the reflected operation,
|
||||
/// then falls back to identity for Eq/Ne or rejects ordering.
|
||||
fn compare(
|
||||
&self,
|
||||
_other: &dyn ObjectPayload,
|
||||
_operation: CompareOp,
|
||||
) -> Result<Option<bool>, VmError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// 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<Value, VmError> {
|
||||
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<Value, VmError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NativeFunction {
|
||||
name: String,
|
||||
function: NativeCall,
|
||||
}
|
||||
|
||||
impl NativeFunction {
|
||||
pub fn new(name: impl Into<String>, 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<String, VmError> {
|
||||
Ok(format!("<native-function {}>", self.name))
|
||||
}
|
||||
|
||||
fn call(&self, arguments: &[Value]) -> Result<Value, VmError> {
|
||||
(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<dyn ObjectPayload>,
|
||||
pub(crate) attributes: BTreeMap<String, Value>,
|
||||
pub(crate) payload_edges: Vec<ObjectRef>,
|
||||
}
|
||||
|
||||
impl EndorObject {
|
||||
pub fn header(&self) -> &ObjectHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub fn payload(&self) -> &dyn ObjectPayload {
|
||||
self.payload.as_ref()
|
||||
}
|
||||
|
||||
/// Visits owned edges, not external roots. Payload edges are snapshotted at
|
||||
/// allocation; 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<ObjectRef> {
|
||||
let mut edges = Vec::new();
|
||||
self.trace(&mut |reference| edges.push(reference));
|
||||
edges
|
||||
}
|
||||
}
|
||||
+114
-116
@@ -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<Self, VmError> {
|
||||
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<String>,
|
||||
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" || 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<ObjectRef, VmError> {
|
||||
self.heap.allocate(Box::new(payload))
|
||||
}
|
||||
|
||||
pub fn create_instance(&mut self, class: ClassRef) -> Result<InstanceRef, VmError> {
|
||||
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<ObjectRef, VmError> {
|
||||
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<ClassRef, VmError> {
|
||||
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<usize, VmError> {
|
||||
Ok(self.heap.get(reference)?.header().ref_count())
|
||||
}
|
||||
|
||||
pub fn is_same(&self, left: ObjectRef, right: ObjectRef) -> Result<bool, VmError> {
|
||||
self.heap.get(left)?;
|
||||
self.heap.get(right)?;
|
||||
Ok(left == right)
|
||||
}
|
||||
|
||||
pub fn repr(&self, reference: ObjectRef) -> Result<String, VmError> {
|
||||
self.heap.get(reference)?.payload().repr(reference)
|
||||
}
|
||||
|
||||
pub fn str(&self, reference: ObjectRef) -> Result<String, VmError> {
|
||||
self.heap.get(reference)?.payload().str(reference)
|
||||
}
|
||||
|
||||
pub fn is_true(&self, reference: ObjectRef) -> Result<bool, VmError> {
|
||||
self.heap.get(reference)?.payload().is_true()
|
||||
}
|
||||
|
||||
pub fn hash(&self, reference: ObjectRef) -> Result<u64, VmError> {
|
||||
self.heap.get(reference)?.payload().hash(reference)
|
||||
}
|
||||
|
||||
pub fn compare(
|
||||
&self,
|
||||
left: ObjectRef,
|
||||
right: ObjectRef,
|
||||
operation: CompareOp,
|
||||
) -> Result<bool, VmError> {
|
||||
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<Value, VmError> {
|
||||
self.heap.get_attr(reference, name)
|
||||
}
|
||||
|
||||
/// Converts a generic reference only after validating ownership and kind.
|
||||
pub fn as_class(&self, reference: ObjectRef) -> Result<ClassRef, VmError> {
|
||||
let typed = ClassRef(reference);
|
||||
self.class(typed)?;
|
||||
Ok(typed)
|
||||
}
|
||||
|
||||
pub fn as_instance(&self, reference: ObjectRef) -> Result<InstanceRef, VmError> {
|
||||
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<String>,
|
||||
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<Value, VmError> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
+434
-145
@@ -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<AtomicUsize>,
|
||||
edges: Vec<ObjectRef>,
|
||||
}
|
||||
|
||||
#[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<String, VmError> {
|
||||
Ok(self.0.to_string())
|
||||
}
|
||||
|
||||
fn is_true(&self) -> Result<bool, VmError> {
|
||||
Ok(self.0 != 0)
|
||||
}
|
||||
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
Ok(self.0 as u64)
|
||||
}
|
||||
|
||||
fn compare(
|
||||
&self,
|
||||
other: &dyn ObjectPayload,
|
||||
operation: CompareOp,
|
||||
) -> Result<Option<bool>, VmError> {
|
||||
let Some(other) = other.as_any().downcast_ref::<Self>() 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<AtomicUsize>, edges: Vec<ObjectRef>) -> 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<Option<bool>, VmError> {
|
||||
Ok(Some(operation == CompareOp::Gt))
|
||||
}
|
||||
|
||||
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
||||
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<Value, VmError> {
|
||||
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(), "<native-function add>");
|
||||
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<Value, VmError> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<AppState>) -> (StatusCode, Json<Health>) {
|
||||
}
|
||||
|
||||
async fn bootstrap(State(state): State<AppState>) -> Json<BootstrapInfo> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
+27
-19
@@ -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<dyn ObjectPayload>`、托管属性和载荷引用快照。对象头仅保存身份、总强引用数和宿主外部强引用数。操作通过 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<EndorObject>>`。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、堆序列化和浏览器通信。不得在交接中把这些当成已实现契约。
|
||||
|
||||
+11
-7
@@ -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 状态与后续远程
|
||||
|
||||
|
||||
@@ -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<dyn ObjectPayload>`。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;不需要创建一个 Python 式的 Type 对象。`kind_name` 只用于诊断,`as_any` 只用于原生 Rust 扩展判断自己能否处理另一份载荷。
|
||||
|
||||
`attributes` 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。
|
||||
|
||||
`payload_edges` 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。分配后这些载荷边必须保持不变;需要修改的对象关系放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。
|
||||
|
||||
## 指针与句柄
|
||||
|
||||
Heap 使用 `Option<Box<EndorObject>>` 保存槽位,实际对象位于独立分配的 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<Value, VmError>;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 为准,不在本地构建、测试或下载依赖。
|
||||
@@ -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 已成功,但不代表本次修正通过。
|
||||
|
||||
+4
-3
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user