diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index 570f17b..cb87617 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -220,6 +220,20 @@ impl Vm { Self::stack_value(frame, frame.stack.len() - 1) } + fn assignment_value(&self, previous: Option, incoming: Value) -> Result { + if let (Some(Value::Object(old)), Value::Object(new)) = (&previous, &incoming) { + if old == new + || self + .object(*old)? + .payload() + .assign_from(self.object(*new)?.payload())? + { + return Ok(Value::Object(*old)); + } + } + Ok(incoming) + } + fn push_borrowed(&mut self, frame: &mut EndorFrame, value: Value) -> Result<(), VmError> { self.retain_value(&value)?; frame.stack.push(StackEntry::Value(value)); @@ -372,9 +386,12 @@ impl Vm { self.push_borrowed(frame, value)?; } Opcode::StoreFast => { - let value = Self::top_value(frame)?; - // The stack root is moved into the local slot. - frame.stack.pop(); + let value = self.assignment_value( + frame.localsplus[argument].clone(), + Self::top_value(frame)?, + )?; + self.retain_value(&value)?; + self.discard(frame, 1); if let Some(old) = frame.localsplus[argument].replace(value) { self.drop_root(old); } @@ -398,8 +415,11 @@ impl Vm { self.push_borrowed(frame, value)?; } Opcode::StoreGlobal => { - let value = Self::top_value(frame)?; let key = Value::Text(frame.executable.layout().names[argument].clone()); + let value = self.assignment_value( + self.dict_get(frame.globals, &key)?, + Self::top_value(frame)?, + )?; self.dict_set(frame.globals, key, value)?; self.discard(frame, 1); } @@ -426,7 +446,9 @@ impl Vm { } Opcode::StoreDeref => { let cell = self.cell_slot(frame, argument)?; - self.cell_set(cell, Self::top_value(frame)?)?; + let value = + self.assignment_value(self.cell_get(cell).ok(), Self::top_value(frame)?)?; + self.cell_set(cell, value)?; self.discard(frame, 1); } Opcode::DeleteDeref => { diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index 6e611d3..b21bd5f 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -9,3 +9,5 @@ | [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 | 核心对象现由 objects 定义,本目录保留既有 endor_core 导入与 Vm 方法的兼容性检查。独立对象存储检查见 [objects/tests](../../objects/tests/README.md)。堆内部伪造句柄与计数溢出检查在 objects 的 src/heap.rs,公开示例由 src/lib.rs rustdoc 提供。文件职责见 [src](../src/README.md),检查方式和结果边界见 [验证](../../../docs/operations/validation.md)。本地不执行测试;存在测试文件不代表某次提交已通过。返回 [core](../README.md)。 + +execution.rs 同时覆盖宿主可选 assign_from 的全局/局部赋值、别名身份、拒绝替换与执行根清理;不在本地运行。 diff --git a/crates/core/tests/execution.rs b/crates/core/tests/execution.rs index 6cd7bce..b441461 100644 --- a/crates/core/tests/execution.rs +++ b/crates/core/tests/execution.rs @@ -35,6 +35,130 @@ fn return_constant(vm: &mut Vm, value: Value) -> ObjectRef { ) } +#[derive(Debug)] +struct SharedReference(AtomicUsize); +impl ObjectPayload for SharedReference { + fn as_any(&self) -> &dyn Any { + self + } + fn kind_name(&self) -> &'static str { + "shared-reference" + } + fn assign_from(&self, source: &dyn ObjectPayload) -> Result { + let Some(source) = source.as_any().downcast_ref::() else { + return Ok(false); + }; + let value = source.0.load(Ordering::SeqCst); + if value == 99 { + return Err(VmError::InvalidArgument("rejected replacement")); + } + self.0.store(value, Ordering::SeqCst); + Ok(true) + } +} + +#[test] +fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting() { + for local in [false, true] { + let mut vm = Vm::new().unwrap(); + let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap(); + let new = vm.allocate(SharedReference(AtomicUsize::new(2))).unwrap(); + let globals = vm.new_dict().unwrap(); + vm.dict_set(globals, text("alias"), Value::Object(old)) + .unwrap(); + let (store, load) = if local { + (Opcode::StoreFast, Opcode::LoadFast) + } else { + (Opcode::StoreGlobal, Opcode::LoadGlobal) + }; + let code = make_code( + &mut vm, + &[ + op(Opcode::LoadConst, 0), + op(store, 0), + op(Opcode::LoadConst, 1), + op(store, 0), + op(load, 0), + simple(Opcode::ReturnValue), + ], + vec![Value::Object(old), Value::Object(new)], + if local { + CodeLayout { + locals: vec!["value".into()], + ..CodeLayout::default() + } + } else { + CodeLayout { + names: vec!["value".into()], + ..CodeLayout::default() + } + }, + ); + let value = vm.execute(code, globals).unwrap(); + assert_eq!(value, Value::Object(old)); + assert_eq!( + vm.dict_get(globals, &text("alias")).unwrap(), + Some(Value::Object(old)) + ); + assert_eq!( + vm.payload::(old, "shared-reference") + .unwrap() + .0 + .load(Ordering::SeqCst), + 2 + ); + vm.release_value(value).unwrap(); + vm.release(code).unwrap(); + vm.dict_clear(globals).unwrap(); + vm.release(globals).unwrap(); + vm.release(old).unwrap(); + vm.release(new).unwrap(); + assert_eq!(vm.object_count(), 0); + } +} + +#[test] +fn failed_opt_in_assignment_preserves_target_and_cleans_execution_roots() { + let mut vm = Vm::new().unwrap(); + let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap(); + let rejected = vm.allocate(SharedReference(AtomicUsize::new(99))).unwrap(); + let globals = vm.new_dict().unwrap(); + vm.dict_set(globals, text("value"), Value::Object(old)) + .unwrap(); + let code = make_code( + &mut vm, + &[ + op(Opcode::LoadConst, 0), + op(Opcode::StoreGlobal, 0), + op(Opcode::LoadConst, 1), + simple(Opcode::ReturnValue), + ], + vec![Value::Object(rejected), Value::Nil], + CodeLayout { + names: vec!["value".into()], + ..CodeLayout::default() + }, + ); + assert!(vm.execute(code, globals).is_err()); + assert_eq!( + vm.payload::(old, "shared-reference") + .unwrap() + .0 + .load(Ordering::SeqCst), + 1 + ); + assert_eq!( + vm.dict_get(globals, &text("value")).unwrap(), + Some(Value::Object(old)) + ); + vm.release(code).unwrap(); + vm.dict_clear(globals).unwrap(); + vm.release(globals).unwrap(); + vm.release(old).unwrap(); + vm.release(rejected).unwrap(); + assert_eq!(vm.object_count(), 0); +} + #[test] fn wordcode_extended_operands_decode_and_execute_without_truncation() { let encoded = Instruction::encode_all(&[op(Opcode::LoadConst, 0x01020304)]).unwrap(); diff --git a/crates/objects/src/object.rs b/crates/objects/src/object.rs index 25b078f..35a9b91 100644 --- a/crates/objects/src/object.rs +++ b/crates/objects/src/object.rs @@ -96,6 +96,14 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { fn call(&self, _arguments: &[Value]) -> Result { Err(VmError::UnsupportedOperation("call")) } + + /// Opt-in shared-reference assignment. Returning true preserves the existing + /// object identity after replacing its host-owned target. Ordinary payloads + /// return false and retain normal value assignment. Implementations must + /// validate before mutation and must not add untraced managed-object edges. + fn assign_from(&self, _source: &dyn ObjectPayload) -> Result { + Ok(false) + } } #[derive(Debug, Default)] diff --git a/docs/README.md b/docs/README.md index 7576d59..38fc93d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ | 词法、语法、裸赋值和诊断 | [Frontend](runtime/frontend.md) | frontend/src/lexer.peg/grammar.peg | | ASDL、AST 与编辑器接口 | [AST](runtime/ast.md) | frontend/Endor.asdl | | 名称分析、编译和装载 | [Compiler](runtime/compiler.md) | compiler/src | -| 字节码、函数与限额 | [字节码 VM](runtime/bytecode.md) | objects/src、core/src | +| 字节码、函数、共享引用赋值与限额 | [字节码 VM](runtime/bytecode.md) | objects/src、core/src | | 引用与 VM 生命周期 | [生命周期](runtime/lifecycle.md) | objects/src、core/src | | UTF-8 与地区 | [文本编码](runtime/text-encoding.md) | frontend、objects | | 嵌入及宿主入口 | [使用引擎](operations/README.md) | 独立宿主仓库 | diff --git a/docs/contributing/handoff.md b/docs/contributing/handoff.md index 056fac2..4c31fd1 100644 --- a/docs/contributing/handoff.md +++ b/docs/contributing/handoff.md @@ -21,7 +21,7 @@ Aster 是可嵌入引擎,只保留 objects、core、frontend、compiler。Obje ## 当前缺口 -平台资源版本/依赖锁、源码闭包、数值算术、完整异常传播、内存配额、可靠取消、不可信代码沙箱、自动 GC 和宿主 panic 后执行根恢复尚未完成。类型注解只保存语法,不引入 Python type/元类。UI 接入适配由 Tessera 实现,不为 Tessera 修改引擎接口。 +平台资源版本/依赖锁、源码闭包、数值算术、完整异常传播、内存配额、可靠取消、不可信代码沙箱、自动 GC 和宿主 panic 后执行根恢复尚未完成。类型注解只保存语法,不引入 Python type/元类。UI 接入适配由 Tessera 实现。通用 ObjectPayload::assign_from 允许宿主对象选择共享引用赋值;协议和根计数归 Aster,UI 挂载、布局、绘制仍归 Tessera,见字节码 VM 的共享引用章节。 ## 工作约定 diff --git a/docs/runtime/bytecode.md b/docs/runtime/bytecode.md index e8c283b..3c2720a 100644 --- a/docs/runtime/bytecode.md +++ b/docs/runtime/bytecode.md @@ -169,3 +169,9 @@ fn main() -> Result<(), endor_core::VmError> { 验证命令与结果边界统一见 [验证](../operations/validation.md)。限额与环境隔离见 [配置](../operations/configuration.md),数据与校验见 [objects 源码导航](../../crates/objects/src/README.md),执行职责见 [core 源码导航](../../crates/core/src/README.md)。 参考:[CPython 解释器设计](https://github.com/python/cpython/blob/v3.14.0/InternalDocs/interpreter.md)、[执行帧设计](https://github.com/python/cpython/blob/v3.14.0/InternalDocs/frames.md)、[帧结构](https://github.com/python/cpython/blob/v3.14.0/Include/internal/pycore_interpframe_structs.h)、[指令定义](https://github.com/python/cpython/blob/v3.14.0/Python/bytecodes.c)、[求值入口与执行器](https://github.com/python/cpython/blob/v3.14.0/Python/ceval.c)。 + +## 宿主共享引用的赋值协议 + +`ObjectPayload::assign_from(source)` 是可选协议,默认返回 false,普通值与对象仍替换变量绑定。StoreGlobal、StoreFast、StoreDeref 在既有值和新值均为对象时调用此协议;返回 true 表示宿主对象已替换自己的目标,绑定保留旧对象身份,其他别名随共享引用观察更新。相同句柄赋值为无操作。此协议同时适用于这些存储指令实现的声明初始化/赋值;字典 API 与 StoreAttr 不隐式调用它。 + +协议不传递变量名字或 UI 元数据,也不引入渲染器。实现必须先验证再修改,拒绝替换时保留旧目标;不得通过可变 payload 悄悄增加未跟踪的 VM 对象边。宿主原生关系如持有受管理对象,必须通过 VM retain/release 登记根。原生资源的内部引用可以由宿主维护,但其管理对象身份和存活根仍由 Aster 校验和计数。StoreFast 会先持有选择后的值,再释放栈值与旧槽,失败执行仍走统一帧清理。