fix: preserve plain reference rebinding during read-only calls

This commit is contained in:
2026-10-21 14:30:06 +08:00
parent ff49f266a2
commit 4a2f81cb48
9 changed files with 51 additions and 2 deletions
+15
View File
@@ -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
}
+3
View File
@@ -174,6 +174,9 @@ impl EndorCodeObject {
}
impl ObjectPayload for EndorCodeObject {
fn reference_assignment_only(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any {
self
}
+3
View File
@@ -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
}
+3
View File
@@ -276,6 +276,9 @@ impl EndorNumberObject {
}
impl ObjectPayload for EndorNumberObject {
fn reference_assignment_only(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any {
self
}
+13
View File
@@ -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<bool, VmError> {
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
}
+6 -1
View File
@@ -72,13 +72,14 @@ impl ObjectStore {
}
pub fn assign_from(&mut self, target: ObjectRef, source: ObjectRef) -> Result<bool, VmError> {
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()
+6
View File
@@ -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
}
+1
View File
@@ -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
+1 -1
View File
@@ -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 根,代际句柄避免槽位复用误判。读取是对象粒度;宿主须把借用身份转为版本依赖并验证存活,不能据此保活整个对象图。