From 4a2f81cb484c3114d7d9e15777569c1c1b9a78f3 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 21 Oct 2026 14:30:06 +0800 Subject: [PATCH] fix: preserve plain reference rebinding during read-only calls --- crates/objects/src/builtins.rs | 15 +++++++++++++++ crates/objects/src/code.rs | 3 +++ crates/objects/src/native.rs | 3 +++ crates/objects/src/number.rs | 3 +++ crates/objects/src/object.rs | 13 +++++++++++++ crates/objects/src/store.rs | 7 ++++++- crates/objects/src/types.rs | 6 ++++++ crates/objects/tests/store.rs | 1 + docs/runtime/native-calls.md | 2 +- 9 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/objects/src/builtins.rs b/crates/objects/src/builtins.rs index 3e7f07b..525bb69 100644 --- a/crates/objects/src/builtins.rs +++ b/crates/objects/src/builtins.rs @@ -28,6 +28,9 @@ impl EndorStringObject { } impl ObjectPayload for EndorStringObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } @@ -87,6 +90,9 @@ impl EndorDictObject { } impl ObjectPayload for EndorDictObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } @@ -126,6 +132,9 @@ impl EndorCellObject { } impl ObjectPayload for EndorCellObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } @@ -199,6 +208,9 @@ impl EndorFunctionObject { } impl ObjectPayload for EndorFunctionObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } @@ -302,6 +314,9 @@ impl EndorBaseExceptionObject { } impl ObjectPayload for EndorBaseExceptionObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } diff --git a/crates/objects/src/code.rs b/crates/objects/src/code.rs index 7a14c84..63337b0 100644 --- a/crates/objects/src/code.rs +++ b/crates/objects/src/code.rs @@ -174,6 +174,9 @@ impl EndorCodeObject { } impl ObjectPayload for EndorCodeObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } diff --git a/crates/objects/src/native.rs b/crates/objects/src/native.rs index c6ff12a..937f10d 100644 --- a/crates/objects/src/native.rs +++ b/crates/objects/src/native.rs @@ -203,6 +203,9 @@ impl ContextNativeFunction { } impl ObjectPayload for ContextNativeFunction { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn std::any::Any { self } diff --git a/crates/objects/src/number.rs b/crates/objects/src/number.rs index 9da9f6b..f1d09b3 100644 --- a/crates/objects/src/number.rs +++ b/crates/objects/src/number.rs @@ -276,6 +276,9 @@ impl EndorNumberObject { } impl ObjectPayload for EndorNumberObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } diff --git a/crates/objects/src/object.rs b/crates/objects/src/object.rs index 955079b..86f1883 100644 --- a/crates/objects/src/object.rs +++ b/crates/objects/src/object.rs @@ -120,6 +120,13 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { /// 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. + /// True only when legacy assign_from always returns false. Read-only + /// callers can then rebind a local reference without attempting mutation. + /// assignment_cell is checked first and remains a protected write protocol. + fn reference_assignment_only(&self) -> bool { + false + } + fn assign_from(&self, _source: &dyn ObjectPayload) -> Result { Ok(false) } @@ -141,6 +148,9 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { pub struct Record; impl ObjectPayload for Record { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } @@ -180,6 +190,9 @@ impl EndorNativeFunctionObject { pub type NativeFunction = EndorNativeFunctionObject; impl ObjectPayload for EndorNativeFunctionObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn Any { self } diff --git a/crates/objects/src/store.rs b/crates/objects/src/store.rs index 5bd5db8..dd81d7c 100644 --- a/crates/objects/src/store.rs +++ b/crates/objects/src/store.rs @@ -72,13 +72,14 @@ impl ObjectStore { } pub fn assign_from(&mut self, target: ObjectRef, source: ObjectRef) -> Result { - self.heap.prepare_change(target)?; + self.object(target)?; self.object(source)?; if let Some(cell) = self .object(target)? .payload() .assignment_cell(source, self)? { + self.heap.prepare_change(target)?; // Replacing the Cell edge may otherwise reclaim a target which is // reachable only through the edge being removed. self.retain(target)?; @@ -89,6 +90,10 @@ impl ObjectStore { self.release(target)?; return result.map(|()| true); } + if self.is_read_only() && self.object(target)?.payload().reference_assignment_only() { + return Ok(false); + } + self.heap.prepare_change(target)?; let changed = self .object(target)? .payload() diff --git a/crates/objects/src/types.rs b/crates/objects/src/types.rs index f75db14..0eaa33a 100644 --- a/crates/objects/src/types.rs +++ b/crates/objects/src/types.rs @@ -50,6 +50,9 @@ impl EndorTypeObject { } impl ObjectPayload for EndorTypeObject { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn std::any::Any { self } @@ -462,6 +465,9 @@ struct BoundMethod { function: ObjectRef, } impl ObjectPayload for BoundMethod { + fn reference_assignment_only(&self) -> bool { + true + } fn as_any(&self) -> &dyn std::any::Any { self } diff --git a/crates/objects/tests/store.rs b/crates/objects/tests/store.rs index 307f844..7ba0ea3 100644 --- a/crates/objects/tests/store.rs +++ b/crates/objects/tests/store.rs @@ -131,6 +131,7 @@ fn read_only_scopes_block_existing_container_and_type_writes_but_allow_fresh_res ); let fresh = store.new_object().unwrap(); store.set_attr(fresh, "value", objects::Value::Nil).unwrap(); + assert!(!store.assign_from(record, fresh).unwrap()); let nested = store.begin_read_only(); assert!( store diff --git a/docs/runtime/native-calls.md b/docs/runtime/native-calls.md index 44359be..8ffcf7e 100644 --- a/docs/runtime/native-calls.md +++ b/docs/runtime/native-calls.md @@ -27,7 +27,7 @@ function 和每个对象参数在移交前取得根;VM 完成调用准备后 ## 只读调用与读取依赖 -Vm::call_read_only_with_limits 沿用显式帧与预算,返回拥有式结果及借用式读取对象身份列表。进入时现存对象的属性、载荷、容器、Cell、类型边与共享赋值写入均在 prepare_change 屏障前拒绝;调用内新分配的结果图可以正常构造。作用域正常返回或报错后恢复;这不提供 panic 恢复或外部 I/O 沙箱。 +Vm::call_read_only_with_limits 沿用显式帧与预算,返回拥有式结果及借用式读取对象身份列表。进入时现存对象的属性、载荷、容器、Cell、类型边与共享赋值写入均在 prepare_change 屏障前拒绝;调用内新分配的结果图可以正常构造。局部变量对普通引用的重新绑定仍可进行:已知无可变 legacy assign_from 的载荷声明 reference_assignment_only=true,共享 assignment_cell 写入仍先检查屏障;未知宿主载荷默认保守拒绝。作用域正常返回或报错后恢复;这不提供 panic 恢复或外部 I/O 沙箱。 ObjectStore 的 begin_read_only/end_read_only 提供同样的 LIFO 嵌套机制,is_read_only 供宿主协议检查。保护集合与读取集合不持有 GC 根,代际句柄避免槽位复用误判。读取是对象粒度;宿主须把借用身份转为版本依赖并验证存活,不能据此保活整个对象图。