Prove read-only effects through current source call graphs
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
| [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 |
|
||||
| [types.rs](types.rs) | 名义身份、继承与实例关系、类型边保活/循环回收、只读类型元数据与外来句柄、显式Trait身份/继承/参数约束及后代更新原子性;统一函数分类、原生数量元数据/副作用前拒绝/能力更新/默认参数与返回类型错误根清理;仅远端执行 |
|
||||
| [typed_traits.rs](typed_traits.rs) | 类型化Trait登记、原子拒绝、源码检查关闭时的显式约束、宽参数调用、类型强边GC、体后实际能力结果检查与临时验证集合恢复;仅远端执行 |
|
||||
| [method_contracts.rs](method_contracts.rs) | 源码/原生方法覆写参数逆变/返回协变、联合参数、已证明源码只读/原生效果及共享赋值保守分类、祖先替换原子性、动态名称重绑定/前向名称与体前拒绝/恢复及清理;仅远端执行 |
|
||||
| [method_contracts.rs](method_contracts.rs) | 源码/原生方法覆写参数逆变/返回协变、联合参数、已证明源码/调用图只读、全局/闭包/接收者重绑定、控制流汇合与原生效果及共享赋值保守分类、祖先替换原子性、动态名称重绑定/前向名称与体前拒绝/恢复及清理;仅远端执行 |
|
||||
| [native_context.rs](native_context.rs) | 原生作用域根、分配失败清理、原生签名/联合与可空类型/固定与Delegated效果和委托返回根、绑定方法元数据、委托、只读结果分配/写入拒绝/依赖/校验与普通读取区分及失败恢复、弱引用过期及有界变更;仅远端执行 |
|
||||
|
||||
核心对象现由 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)。
|
||||
|
||||
@@ -588,3 +588,289 @@ fn shared_local_assignment_and_dynamic_calls_are_not_inferred_pure() {
|
||||
.unwrap();
|
||||
assert!(!call.is_statically_read_only());
|
||||
}
|
||||
|
||||
fn global_caller(vm: &mut Vm, globals: ObjectRef, name: &str) -> ObjectRef {
|
||||
let code = EndorCode::from_instructions(
|
||||
&[
|
||||
Instruction::new(Opcode::LoadGlobal, 0),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::Call, 0),
|
||||
Instruction::simple(Opcode::ReturnValue),
|
||||
],
|
||||
vec![],
|
||||
CodeLayout {
|
||||
names: vec![name.into()],
|
||||
..CodeLayout::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let code = vm.new_code(code).unwrap();
|
||||
let function = vm
|
||||
.new_function("caller", code, globals, vec![], vec![])
|
||||
.unwrap();
|
||||
vm.release(code).unwrap();
|
||||
function
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transitive_effect_queries_follow_rebinding_closures_and_recursive_graphs() {
|
||||
let mut vm = Vm::new().unwrap();
|
||||
let globals = vm.new_dict().unwrap();
|
||||
let builtins = vm.new_dict().unwrap();
|
||||
vm.dict_set(
|
||||
globals,
|
||||
Value::Text("__builtins__".into()),
|
||||
Value::Object(builtins),
|
||||
)
|
||||
.unwrap();
|
||||
let pure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"pure",
|
||||
Native(CallSignature::exact(
|
||||
vec![],
|
||||
T::Number,
|
||||
CallEffect::ReadOnly,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
let impure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"impure",
|
||||
Native(CallSignature::exact(
|
||||
vec![],
|
||||
T::Number,
|
||||
CallEffect::Mutating,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
vm.dict_set(builtins, Value::Text("target".into()), Value::Object(pure))
|
||||
.unwrap();
|
||||
let wrapper = global_caller(&mut vm, globals, "target");
|
||||
let outer = global_caller(&mut vm, globals, "wrapper");
|
||||
vm.dict_set(
|
||||
globals,
|
||||
Value::Text("wrapper".into()),
|
||||
Value::Object(wrapper),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(outer).unwrap(), Some(CallEffect::ReadOnly));
|
||||
vm.dict_set(globals, Value::Text("target".into()), Value::Object(impure))
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(outer).unwrap(), None);
|
||||
vm.dict_delete(globals, &Value::Text("target".into()))
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(outer).unwrap(), Some(CallEffect::ReadOnly));
|
||||
let cell = vm.new_cell(Some(Value::Object(pure))).unwrap();
|
||||
let code = EndorCode::from_instructions(
|
||||
&[
|
||||
Instruction::new(Opcode::LoadDeref, 0),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::Call, 0),
|
||||
Instruction::simple(Opcode::ReturnValue),
|
||||
],
|
||||
vec![],
|
||||
CodeLayout {
|
||||
free_variables: vec!["target".into()],
|
||||
..CodeLayout::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let code = vm.new_code(code).unwrap();
|
||||
let closure = vm
|
||||
.new_function("closure", code, globals, vec![], vec![cell])
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(closure).unwrap(), Some(CallEffect::ReadOnly));
|
||||
vm.cell_set(cell, Value::Object(impure)).unwrap();
|
||||
assert_eq!(vm.call_effect(closure).unwrap(), None);
|
||||
vm.cell_set(cell, Value::Object(pure)).unwrap();
|
||||
assert_eq!(vm.call_effect(closure).unwrap(), Some(CallEffect::ReadOnly));
|
||||
let recursive = global_caller(&mut vm, globals, "recursive");
|
||||
vm.dict_set(
|
||||
globals,
|
||||
Value::Text("recursive".into()),
|
||||
Value::Object(recursive),
|
||||
)
|
||||
.unwrap();
|
||||
// No function body is executed, including this non-terminating call graph.
|
||||
assert_eq!(
|
||||
vm.call_effect(recursive).unwrap(),
|
||||
Some(CallEffect::ReadOnly)
|
||||
);
|
||||
vm.dict_set(
|
||||
globals,
|
||||
Value::Text("recursive".into()),
|
||||
Value::Object(impure),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(recursive).unwrap(), None);
|
||||
for object in [
|
||||
recursive, closure, code, cell, outer, wrapper, pure, impure, builtins, globals,
|
||||
] {
|
||||
vm.release(object).unwrap();
|
||||
}
|
||||
vm.collect_cycles();
|
||||
assert_eq!(vm.object_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_control_flow_joins_do_not_keep_a_stale_known_call_target() {
|
||||
let mut vm = Vm::new().unwrap();
|
||||
let globals = vm.new_dict().unwrap();
|
||||
let pure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"pure",
|
||||
Native(CallSignature::exact(
|
||||
vec![],
|
||||
T::Number,
|
||||
CallEffect::ReadOnly,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
let impure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"impure",
|
||||
Native(CallSignature::exact(
|
||||
vec![],
|
||||
T::Number,
|
||||
CallEffect::External,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
for name in ["left", "right"] {
|
||||
vm.dict_set(globals, Value::Text(name.into()), Value::Object(pure))
|
||||
.unwrap();
|
||||
}
|
||||
let code = EndorCode::from_instructions(
|
||||
&[
|
||||
Instruction::new(Opcode::LoadFast, 0),
|
||||
Instruction::new(Opcode::PopJumpIfFalse, 2),
|
||||
Instruction::new(Opcode::LoadGlobal, 0),
|
||||
Instruction::new(Opcode::JumpForward, 1),
|
||||
Instruction::new(Opcode::LoadGlobal, 1),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::Call, 0),
|
||||
Instruction::simple(Opcode::ReturnValue),
|
||||
],
|
||||
vec![],
|
||||
CodeLayout {
|
||||
parameters: vec!["condition".into()],
|
||||
locals: vec!["condition".into()],
|
||||
names: vec!["left".into(), "right".into()],
|
||||
..CodeLayout::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let code = vm.new_code(code).unwrap();
|
||||
let function = vm
|
||||
.new_function("choose", code, globals, vec![], vec![])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.call_effect(function).unwrap(),
|
||||
Some(CallEffect::ReadOnly)
|
||||
);
|
||||
vm.dict_set(globals, Value::Text("right".into()), Value::Object(impure))
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(function).unwrap(), None);
|
||||
vm.dict_set(globals, Value::Text("right".into()), Value::Object(pure))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.call_effect(function).unwrap(),
|
||||
Some(CallEffect::ReadOnly)
|
||||
);
|
||||
for object in [function, code, pure, impure, globals] {
|
||||
vm.release(object).unwrap();
|
||||
}
|
||||
vm.collect_cycles();
|
||||
assert_eq!(vm.object_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_source_effects_follow_the_actual_receiver_method_and_instance_field() {
|
||||
let mut vm = Vm::new().unwrap();
|
||||
let roots = vm.bootstrap_types().unwrap();
|
||||
let globals = vm.new_dict().unwrap();
|
||||
let definition = vm
|
||||
.new_type("Receiver", roots.object, TypeConstructor::Record)
|
||||
.unwrap();
|
||||
let pure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"helper",
|
||||
Native(CallSignature::exact(
|
||||
vec![T::Any],
|
||||
T::Number,
|
||||
CallEffect::ReadOnly,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
let impure = vm
|
||||
.allocate(ContextNativeFunction::new(
|
||||
"helper",
|
||||
Native(CallSignature::exact(
|
||||
vec![T::Any],
|
||||
T::Number,
|
||||
CallEffect::Mutating,
|
||||
)),
|
||||
vec![],
|
||||
))
|
||||
.unwrap();
|
||||
let code = EndorCode::from_instructions(
|
||||
&[
|
||||
Instruction::new(Opcode::LoadFast, 0),
|
||||
Instruction::new(Opcode::LoadAttr, 0),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::Call, 0),
|
||||
Instruction::simple(Opcode::ReturnValue),
|
||||
],
|
||||
vec![],
|
||||
CodeLayout {
|
||||
parameters: vec!["self".into()],
|
||||
locals: vec!["self".into()],
|
||||
names: vec!["helper".into()],
|
||||
..CodeLayout::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let code = vm.new_code(code).unwrap();
|
||||
let function = vm
|
||||
.new_function("check", code, globals, vec![], vec![])
|
||||
.unwrap();
|
||||
vm.define_method(definition, "helper", pure).unwrap();
|
||||
vm.define_method(definition, "check", function).unwrap();
|
||||
let receiver = vm.new_object().unwrap();
|
||||
vm.attach_type(receiver, definition).unwrap();
|
||||
let Value::Object(bound) = vm.bind_attr(receiver, "check").unwrap() else {
|
||||
panic!("bound")
|
||||
};
|
||||
assert_eq!(vm.call_effect(function).unwrap(), None);
|
||||
assert_eq!(vm.call_effect(bound).unwrap(), Some(CallEffect::ReadOnly));
|
||||
vm.define_method(definition, "helper", impure).unwrap();
|
||||
assert_eq!(vm.call_effect(bound).unwrap(), None);
|
||||
vm.define_method(definition, "helper", pure).unwrap();
|
||||
assert_eq!(vm.call_effect(bound).unwrap(), Some(CallEffect::ReadOnly));
|
||||
vm.set_attr(receiver, "helper", Value::Object(impure))
|
||||
.unwrap();
|
||||
assert_eq!(vm.call_effect(bound).unwrap(), None);
|
||||
vm.del_attr(receiver, "helper").unwrap();
|
||||
assert_eq!(vm.call_effect(bound).unwrap(), Some(CallEffect::ReadOnly));
|
||||
for object in [
|
||||
bound,
|
||||
receiver,
|
||||
function,
|
||||
code,
|
||||
pure,
|
||||
impure,
|
||||
definition,
|
||||
globals,
|
||||
roots.object,
|
||||
roots.type_type,
|
||||
] {
|
||||
vm.release(object).unwrap();
|
||||
}
|
||||
vm.collect_cycles();
|
||||
assert_eq!(vm.object_count(), 0);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# objects
|
||||
|
||||
Endor 核心对象与对象存储包,零外部依赖,不依赖 core。包含 EndorObject、ObjectPayload、Value、ObjectRef、Number、字符串/字典/Cell/函数/异常载荷,以及代码对象与 wordcode 数据定义、编解码和静态验证。提供显式引导的 Object/Type 名义类型,已接入普通源码 class 和继承方法分派,已支持显式 NativeTyped 分配继承和可转移字段存储;trait 与完整元类行为仍待实现。
|
||||
Endor 核心对象与对象存储包,零外部依赖,不依赖 core。包含 EndorObject、ObjectPayload、Value、ObjectRef、Number、字符串/字典/Cell/函数/异常载荷,以及代码对象与 wordcode 数据定义、编解码和静态验证。提供显式引导的 Object/Type 名义类型,已接入普通源码 class 和继承方法分派,已支持显式 NativeTyped 分配继承和可转移字段存储;显式/类型化Trait和保守效果查询已接入,正式trait语法与完整元类行为仍待实现。
|
||||
|
||||
| 内容 | 职责 |
|
||||
| --- | --- |
|
||||
| Cargo.toml | objects 包元数据和工作区 lint;无依赖 |
|
||||
| [src](src/README.md) | 对象定义、私有堆、ObjectStore、对象工厂、字节码数据与共享错误 |
|
||||
| [src](src/README.md) | 对象定义、私有堆、ObjectStore、对象工厂、字节码数据、保守只读调用图分析与共享错误 |
|
||||
| [tests](tests/README.md) | 不依赖 VM 的对象存储、事务失败、回收和共享代码检查 |
|
||||
|
||||
ObjectStore 独立拥有空堆,负责对象分配、代际校验、引用计数、循环回收、属性与内置容器操作;底层 Heap 与句柄字段保持私有。replace_payload 通过公开的受校验事务替换载荷及所有托管边;NativeContext 提供受限原生分配与变更能力。代码对象只保存及验证执行数据,求值循环与调用帧属于 [core](../core/README.md)。共享 VmError 定义在本包,保留现有名称以避免对象操作接口依赖 VM。
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
| [types.rs](types.rs) | 托管名义类型、显式 Object/Type 引导、继承查询、受管方法表/接收者绑定、类字段、显式/类型化Trait定义、强类型签名边与声明约束、实例方法遮蔽检查和宿主缓存依赖名、原生构造委托、统一源码/原生/绑定方法参数区间和已知效果查询与能力验证、声明环境托管边、字段构造/写入检查与标记为校验读取的注解解析/匹配 |
|
||||
| [contracts.rs](contracts.rs) | 方法参数逆变/返回协变、名义/能力/联合包含关系、祖先方法更新的后代检查、动态注解调用前复查、类型化能力替换/返回约束与临时递归验证集合 |
|
||||
| [native.rs](native.rs) | NativeContext、CallArity/CallSignature 类型、Delegated/固定效果元数据、调用前检查、签名类型强边、上下文原生函数、作用域语言函数工厂、显式能力声明入口、函数标记、根与非递归委托调用 |
|
||||
| [effects.rs](effects.rs) | 非递归只读调用图证明、当前全局/闭包/绑定接收者解析、控制流栈事实合并,无执行或隐藏强根 |
|
||||
| [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 |
|
||||
| [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 |
|
||||
| [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边、语言函数标记 |
|
||||
|
||||
+32
-27
@@ -111,33 +111,9 @@ impl EndorCode {
|
||||
/// Unknown calls and writes remain unknown, even when they could be safe for
|
||||
/// a particular invocation. New opcodes require an explicit audit here.
|
||||
pub fn is_statically_read_only(&self) -> bool {
|
||||
self.instructions.iter().all(|decoded| {
|
||||
matches!(
|
||||
decoded.instruction.opcode,
|
||||
Opcode::Nop
|
||||
| Opcode::LoadConst
|
||||
| Opcode::LoadFast
|
||||
| Opcode::DeleteFast
|
||||
| Opcode::LoadGlobal
|
||||
| Opcode::PopTop
|
||||
| Opcode::Copy
|
||||
| Opcode::Swap
|
||||
| Opcode::PushNull
|
||||
| Opcode::ReturnValue
|
||||
| Opcode::JumpForward
|
||||
| Opcode::JumpBackward
|
||||
| Opcode::PopJumpIfFalse
|
||||
| Opcode::PopJumpIfTrue
|
||||
| Opcode::ToBool
|
||||
| Opcode::UnaryNot
|
||||
| Opcode::CompareOp
|
||||
| Opcode::LoadDeref
|
||||
| Opcode::LoadClosure
|
||||
| Opcode::LoadAttr
|
||||
| Opcode::BuildMap
|
||||
| Opcode::MakeFunction
|
||||
)
|
||||
})
|
||||
self.instructions
|
||||
.iter()
|
||||
.all(|decoded| read_only_opcode(decoded.instruction.opcode))
|
||||
}
|
||||
|
||||
pub fn with_location(
|
||||
@@ -262,6 +238,35 @@ impl ObjectPayload for EndorCodeObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Shared by the local proof and call-graph analysis. Unknown opcodes fail closed.
|
||||
pub(crate) fn read_only_opcode(opcode: Opcode) -> bool {
|
||||
matches!(
|
||||
opcode,
|
||||
Opcode::Nop
|
||||
| Opcode::LoadConst
|
||||
| Opcode::LoadFast
|
||||
| Opcode::DeleteFast
|
||||
| Opcode::LoadGlobal
|
||||
| Opcode::PopTop
|
||||
| Opcode::Copy
|
||||
| Opcode::Swap
|
||||
| Opcode::PushNull
|
||||
| Opcode::ReturnValue
|
||||
| Opcode::JumpForward
|
||||
| Opcode::JumpBackward
|
||||
| Opcode::PopJumpIfFalse
|
||||
| Opcode::PopJumpIfTrue
|
||||
| Opcode::ToBool
|
||||
| Opcode::UnaryNot
|
||||
| Opcode::CompareOp
|
||||
| Opcode::LoadDeref
|
||||
| Opcode::LoadClosure
|
||||
| Opcode::LoadAttr
|
||||
| Opcode::BuildMap
|
||||
| Opcode::MakeFunction
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
|
||||
for names in [
|
||||
&layout.parameters,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! Snapshot proof of read-only call graphs. All identities are borrowed and all
|
||||
//! traversals iterative; analysis neither executes code nor allocates VM objects.
|
||||
use super::{AttributeLookup, BoundMethod};
|
||||
use crate::{
|
||||
CallEffect, EndorCodeObject, EndorFunctionObject, ObjectRef, ObjectStore, Opcode, Value,
|
||||
VmError,
|
||||
};
|
||||
use std::collections::{BTreeSet, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Target {
|
||||
function: ObjectRef,
|
||||
receiver: Option<ObjectRef>,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Fact {
|
||||
Unknown,
|
||||
Null,
|
||||
Object(ObjectRef),
|
||||
Method(Target),
|
||||
}
|
||||
impl Fact {
|
||||
fn value(value: Value) -> Self {
|
||||
match value {
|
||||
Value::Object(object) => Self::Object(object),
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
fn object(self) -> Option<ObjectRef> {
|
||||
match self {
|
||||
Self::Object(object) => Some(object),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectStore {
|
||||
fn effect_target(&self, mut target: Target) -> Result<Target, VmError> {
|
||||
loop {
|
||||
let payload = self.object(target.function)?.payload();
|
||||
let Some(bound) = payload.as_any().downcast_ref::<BoundMethod>() else {
|
||||
return Ok(target);
|
||||
};
|
||||
target = Target {
|
||||
function: bound.function,
|
||||
receiver: Some(bound.receiver),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn infer_call_effect(
|
||||
&self,
|
||||
function: ObjectRef,
|
||||
) -> Result<Option<CallEffect>, VmError> {
|
||||
let root = self.effect_target(Target {
|
||||
function,
|
||||
receiver: None,
|
||||
})?;
|
||||
if !self
|
||||
.object(root.function)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.is::<EndorFunctionObject>()
|
||||
{
|
||||
return Ok(self.native_signature(root.function)?.and_then(|signature| {
|
||||
(signature.effect != CallEffect::Delegated).then_some(signature.effect)
|
||||
}));
|
||||
}
|
||||
let mut pending = VecDeque::from([root]);
|
||||
let mut visited = BTreeSet::new();
|
||||
while let Some(target) = pending.pop_front() {
|
||||
let target = self.effect_target(target)?;
|
||||
if !visited.insert(target) {
|
||||
continue;
|
||||
}
|
||||
if let Some(source) = self
|
||||
.object(target.function)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.downcast_ref::<EndorFunctionObject>()
|
||||
{
|
||||
let Some(callees) = self.read_only_callees(source, target.receiver)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
pending.extend(callees);
|
||||
} else if self
|
||||
.native_signature(target.function)?
|
||||
.is_none_or(|signature| signature.effect != CallEffect::ReadOnly)
|
||||
{
|
||||
// An impure target could rebind later globals/Cells/methods, so
|
||||
// snapshot edges cannot bound this source invocation's effects.
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Ok(Some(CallEffect::ReadOnly))
|
||||
}
|
||||
|
||||
fn read_only_callees(
|
||||
&self,
|
||||
function: &EndorFunctionObject,
|
||||
receiver: Option<ObjectRef>,
|
||||
) -> Result<Option<BTreeSet<Target>>, VmError> {
|
||||
let code = self
|
||||
.payload::<EndorCodeObject>(function.code(), "code")?
|
||||
.code();
|
||||
if code.is_statically_read_only() {
|
||||
return Ok(Some(BTreeSet::new()));
|
||||
}
|
||||
let instructions = code.instructions();
|
||||
if instructions.iter().any(|decoded| {
|
||||
decoded.instruction.opcode != Opcode::Call
|
||||
&& !crate::code::read_only_opcode(decoded.instruction.opcode)
|
||||
}) {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut states: Vec<Option<Vec<Fact>>> = vec![None; instructions.len()];
|
||||
states[0] = Some(vec![]);
|
||||
let mut work = VecDeque::from([0]);
|
||||
let mut callees = BTreeSet::new();
|
||||
while let Some(index) = work.pop_front() {
|
||||
let decoded = instructions[index];
|
||||
let opcode = decoded.instruction.opcode;
|
||||
let argument = decoded.instruction.argument as usize;
|
||||
let mut stack = states[index].clone().expect("reachable effect state");
|
||||
match opcode {
|
||||
Opcode::LoadConst => stack.push(Fact::value(code.constants()[argument].clone())),
|
||||
Opcode::LoadFast => stack.push(if argument == 0 {
|
||||
receiver.map(Fact::Object).unwrap_or(Fact::Unknown)
|
||||
} else {
|
||||
Fact::Unknown
|
||||
}),
|
||||
Opcode::LoadGlobal => {
|
||||
let key = Value::Text(code.layout().names[argument].clone());
|
||||
let mut value = self.dict_get(function.globals(), &key)?;
|
||||
if value.is_none()
|
||||
&& let Some(builtins) = function.builtins()
|
||||
{
|
||||
value = self.dict_get(builtins, &key)?;
|
||||
}
|
||||
stack.push(value.map(Fact::value).unwrap_or(Fact::Unknown));
|
||||
}
|
||||
Opcode::LoadDeref | Opcode::LoadClosure => {
|
||||
let fact = if argument >= code.layout().locals.len() {
|
||||
let cell = *function
|
||||
.closure()
|
||||
.get(argument - code.layout().locals.len())
|
||||
.ok_or(VmError::InvalidArgument(
|
||||
"function closure does not match code",
|
||||
))?;
|
||||
if opcode == Opcode::LoadClosure {
|
||||
Fact::Object(cell)
|
||||
} else {
|
||||
match self.cell_get(cell) {
|
||||
Ok(value) => Fact::value(value),
|
||||
Err(VmError::EmptyCell) => Fact::Unknown,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
} else if opcode == Opcode::LoadDeref && argument == 0 {
|
||||
receiver.map(Fact::Object).unwrap_or(Fact::Unknown)
|
||||
} else {
|
||||
Fact::Unknown
|
||||
};
|
||||
stack.push(fact);
|
||||
}
|
||||
Opcode::PushNull => stack.push(Fact::Null),
|
||||
Opcode::Copy => stack.push(stack[stack.len() - argument]),
|
||||
Opcode::Swap => {
|
||||
let length = stack.len();
|
||||
stack.swap(length - 1, length - argument);
|
||||
}
|
||||
Opcode::PopTop
|
||||
| Opcode::PopJumpIfFalse
|
||||
| Opcode::PopJumpIfTrue
|
||||
| Opcode::ReturnValue => {
|
||||
stack.pop();
|
||||
}
|
||||
Opcode::ToBool | Opcode::UnaryNot => {
|
||||
*stack.last_mut().unwrap() = Fact::Unknown;
|
||||
}
|
||||
Opcode::CompareOp => {
|
||||
stack.pop();
|
||||
*stack.last_mut().unwrap() = Fact::Unknown;
|
||||
}
|
||||
Opcode::LoadAttr => {
|
||||
let fact = stack.last_mut().unwrap();
|
||||
*fact = if let Some(owner) = fact.object() {
|
||||
match self.lookup_attr(owner, &code.layout().names[argument]) {
|
||||
Ok(AttributeLookup::Value(value)) => Fact::value(value),
|
||||
Ok(AttributeLookup::Method { function, receiver }) => {
|
||||
Fact::Method(Target {
|
||||
function,
|
||||
receiver: Some(receiver),
|
||||
})
|
||||
}
|
||||
Err(
|
||||
VmError::AttributeNotFound(_)
|
||||
| VmError::UnsupportedOperation("attributes"),
|
||||
) => Fact::Unknown,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
} else {
|
||||
Fact::Unknown
|
||||
};
|
||||
}
|
||||
Opcode::Call => {
|
||||
let start = stack.len() - argument - 2;
|
||||
let implicit = if stack[start + 1] == Fact::Null {
|
||||
(argument > 0)
|
||||
.then(|| stack[start + 2])
|
||||
.and_then(Fact::object)
|
||||
} else {
|
||||
stack[start + 1].object()
|
||||
};
|
||||
let target = match stack[start] {
|
||||
Fact::Object(function) => Target {
|
||||
function,
|
||||
receiver: implicit,
|
||||
},
|
||||
Fact::Method(target) => target,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
callees.insert(target);
|
||||
stack.truncate(start);
|
||||
stack.push(Fact::Unknown);
|
||||
}
|
||||
Opcode::BuildMap => {
|
||||
stack.truncate(stack.len() - argument * 2);
|
||||
stack.push(Fact::Unknown);
|
||||
}
|
||||
Opcode::MakeFunction => {
|
||||
let (defaults, closure) =
|
||||
crate::Instruction::function_counts(decoded.instruction.argument);
|
||||
stack.truncate(stack.len() - defaults - closure - 1);
|
||||
stack.push(Fact::Unknown);
|
||||
}
|
||||
Opcode::Nop | Opcode::DeleteFast | Opcode::JumpForward | Opcode::JumpBackward => {}
|
||||
_ => unreachable!("unproved opcodes were rejected before analysis"),
|
||||
}
|
||||
let successors = match opcode {
|
||||
Opcode::ReturnValue => vec![],
|
||||
Opcode::JumpForward | Opcode::JumpBackward => {
|
||||
vec![crate::code::jump_target(decoded)?]
|
||||
}
|
||||
Opcode::PopJumpIfFalse | Opcode::PopJumpIfTrue => {
|
||||
vec![crate::code::jump_target(decoded)?, decoded.next_offset]
|
||||
}
|
||||
_ => vec![decoded.next_offset],
|
||||
};
|
||||
for offset in successors {
|
||||
let next = instructions
|
||||
.binary_search_by_key(&offset, |item| item.offset)
|
||||
.expect("validated successor");
|
||||
let changed = if let Some(existing) = &mut states[next] {
|
||||
let mut changed = false;
|
||||
for (old, new) in existing.iter_mut().zip(&stack) {
|
||||
if *old != *new && *old != Fact::Unknown {
|
||||
*old = Fact::Unknown;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed
|
||||
} else {
|
||||
states[next] = Some(stack.clone());
|
||||
true
|
||||
};
|
||||
if changed {
|
||||
work.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(callees))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Nominal type identities in the same managed heap as their instances.
|
||||
#[path = "contracts.rs"]
|
||||
mod contracts;
|
||||
#[path = "effects.rs"]
|
||||
mod effects;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
@@ -486,30 +488,11 @@ impl ObjectStore {
|
||||
}
|
||||
|
||||
/// Known whole-call effect, after unwrapping bound receivers. Delegated
|
||||
/// native wrappers and source code with unproved operations remain unknown.
|
||||
/// This query does not execute code or freeze dynamic globals/call targets.
|
||||
/// native wrappers and source call graphs with unproved operations remain unknown.
|
||||
/// This query follows current globals, closure Cells and known receivers without
|
||||
/// executing code, freezing targets or retaining a cross-query cache.
|
||||
pub fn call_effect(&self, function: ObjectRef) -> Result<Option<crate::CallEffect>, VmError> {
|
||||
let mut current = function;
|
||||
loop {
|
||||
let payload = self.object(current)?.payload();
|
||||
if let Some(bound) = payload.as_any().downcast_ref::<BoundMethod>() {
|
||||
current = bound.function;
|
||||
continue;
|
||||
}
|
||||
if let Some(source) = payload
|
||||
.as_any()
|
||||
.downcast_ref::<crate::EndorFunctionObject>()
|
||||
{
|
||||
return Ok(self
|
||||
.payload::<crate::EndorCodeObject>(source.code(), "code")?
|
||||
.code()
|
||||
.is_statically_read_only()
|
||||
.then_some(crate::CallEffect::ReadOnly));
|
||||
}
|
||||
return Ok(self.native_signature(current)?.and_then(|signature| {
|
||||
(signature.effect != crate::CallEffect::Delegated).then_some(signature.effect)
|
||||
}));
|
||||
}
|
||||
self.infer_call_effect(function)
|
||||
}
|
||||
|
||||
/// Unified source/native positional interval, after any bound receivers.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
| 解释执行、资源修订与引擎升级 | [执行与版本](architecture/execution-and-versions.md) | frontend/compiler/core |
|
||||
| EndorObject、句柄、默认操作 | [对象模型](runtime/objects.md) | objects/src |
|
||||
| 原生上下文、只读调用、子调用读取依赖及校验分类、统一参数数量/类型/联合/固定与委托效果、委托返回与变更通知 | [原生调用](runtime/native-calls.md) | objects/src/native.rs、changes.rs、core/src/eval.rs |
|
||||
| 托管类型、宿主类注册、原生分配继承、字段存储、显式/类型化能力、函数分类/已知效果、保守源码只读证明、声明字段/源码注解与方法覆写检查 | [名义类型/方法覆写](runtime/types.md) | objects/src/types.rs、contracts.rs |
|
||||
| 托管类型、宿主类注册、原生分配继承、字段存储、显式/类型化能力、函数分类/已知效果、保守源码/调用图只读证明、声明字段/源码注解与方法覆写检查 | [名义类型/方法覆写](runtime/types.md) | objects/src/types.rs、contracts.rs |
|
||||
| Number、String、Dict、Cell、Function | [基本对象](runtime/builtins.md) | objects/src |
|
||||
| 源码读取与来源 | [Reader](runtime/reader.md) | frontend/src/reader.rs |
|
||||
| 词法、语法、裸赋值和诊断 | [Frontend](runtime/frontend.md) | frontend/src/lexer.peg/grammar.peg |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| 文档 | 内容 |
|
||||
| --- | --- |
|
||||
| [types.md](types.md) | 托管 Object/Type、名义继承、方法覆写/类型化Trait/已知效果与源码只读证明/动态检查、声明字段/函数注解检查、构造与类型边生命周期 |
|
||||
| [types.md](types.md) | 托管 Object/Type、名义继承、方法覆写/类型化Trait/已知效果与源码/调用图只读证明/动态检查、声明字段/函数注解检查、构造与类型边生命周期 |
|
||||
| [objects.md](objects.md) | 对象、句柄与默认操作 |
|
||||
| [native-calls.md](native-calls.md) | 原生上下文、读取分类、统一参数数量/类型/联合/固定与委托效果、非递归委托返回、弱引用、托管赋值与变更批次 |
|
||||
| [builtins.md](builtins.md) | Number、String、Dict、Cell、Function、异常数据 |
|
||||
|
||||
@@ -100,7 +100,7 @@ NativeCallable::signature 默认 None,明确表示尚未发布类型/效果契
|
||||
|
||||
NativeResult::Invoke 仍是尾委托,不递归进入 Rust/VM。VM 为每层已声明的返回契约保留检查项;委托原生立即返回或 Endor 帧返回时,按由内到外顺序验证全部契约。返回定义作为执行根保留,原生载荷替换、释放原委托者或源码全局重绑定不会使检查丢失。准备失败、帧限额、指令限额、错误返回与正常完成都释放这些执行根。委托不会为检查额外创建 VM 帧;仍共享每次原生分派和 Endor 指令预算。NativeContext::execute 单独返回 Invoke 时,调用者须使用具有等效最终返回检查的求值器;通常直接使用 Vm::call。
|
||||
|
||||
CallEffect::ReadOnly 声明只读现存对象,可分配临时结果;Delegated 声明原生包装自身只读,但后续委托的整体效果动态确定,VM 保持同一只读/预算作用域并检查实际目标;Mutating 与 External 在 ObjectStore 只读作用域内、进入实现前拒绝。普通执行允许这些效果。ReadOnly 声明不绕过已有写屏障,也不证明可信 Rust 不会进行隐藏 I/O;未知旧原生函数仍沿原调用/宿主检查,不能把 None 理解为纯函数。这不是完整 I/O 沙箱或对阻塞原生代码的抢占。统一已知效果查询和保守源码只读证明见[类型主题](types.md#已知调用效果与保守源码证明)。
|
||||
CallEffect::ReadOnly 声明只读现存对象,可分配临时结果;Delegated 声明原生包装自身只读,但后续委托的整体效果动态确定,VM 保持同一只读/预算作用域并检查实际目标;Mutating 与 External 在 ObjectStore 只读作用域内、进入实现前拒绝。普通执行允许这些效果。ReadOnly 声明不绕过已有写屏障,也不证明可信 Rust 不会进行隐藏 I/O;未知旧原生函数仍沿原调用/宿主检查,不能把 None 理解为纯函数。这不是完整 I/O 沙箱或对阻塞原生代码的抢占。统一已知效果查询和保守源码/调用图只读证明见[类型主题](types.md#已知调用效果与保守源码证明)。
|
||||
|
||||
Tessera 的 measure/arrange/paint/hitTest、Point 几何查询、Window、Block/Container/Page、Mount、Task、Event、Subscription 方法和 ObjectList.at 发布签名。Page.open 的 Window | Unit、Page.window 的 Window | Unit 返回,以及 Block.on 的 Bool | String 可选阶段有准确元数据;Mount 的父/子/同级/Page 弱查询为可空结果。Mount.bounds 现在返回带 Bounds 名义类型的现有几何记录,可直接组合到 Point.inside 或视觉方法。滚动位置/字典等通用记录仍声明 Object,不冒充新语言类型;任务 owner 的具体资源资格仍按原域检查。构造器和其他管理 API 的签名覆盖仍未全部接入。已接入的方法覆写方差与动态名称规则见类型主题。旧能力注册保留数量模式;类型化能力还检查类型/已知效果,并在实际返回和尾委托结果处保留能力返回约束。
|
||||
|
||||
|
||||
@@ -154,8 +154,15 @@ core/tests/typed_traits.rs 覆盖登记失败原子性、参数扩大仍可直
|
||||
|
||||
ObjectStore/Vm::call_effect(function) 返回已知的整体 CallEffect 或 None,解包绑定方法后检查真实目标;原生签名照常验证,Delegated 包装整体未知,旧函数和通用Type构造也不猜测。查询不执行函数,不固定动态全局名称,不新增托管强边。参数/返回类型签名查询仍由 native_signature 和源码注解承担。
|
||||
|
||||
EndorCode::is_statically_read_only 只在全部已验证指令都在只读白名单内时返回true。读取、控制流、局部删除、创建新Dict/Function允许;Call、所有Store、DeleteGlobal/DeleteDeref、BuildClass及未来未审阅指令均保守未知。StoreFast也不能直接视为纯度:已有局部值可能通过共享引用赋值更新原对象。未知不表示一定有写入;创建新结果后填字段或调用纯函数仍可能安全,但本证明不作别名/新鲜性或调用图推断。不可达指令也参与保守扫描,嵌套函数代码不被当作工厂已执行的函数体。
|
||||
EndorCode::is_statically_read_only 只在全部已验证指令都在只读白名单内时返回true。读取、控制流、局部删除、创建新Dict/Function允许;Call、所有Store、DeleteGlobal/DeleteDeref、BuildClass及未来未审阅指令均保守未知。StoreFast也不能直接视为纯度:已有局部值可能通过共享引用赋值更新原对象。未知不表示一定有写入;创建新结果后填字段或调用纯函数仍可能安全,但此局部证明不作别名/新鲜性或调用图推断。不可达指令也参与保守扫描,嵌套函数代码不被当作工厂已执行的函数体。
|
||||
|
||||
源码方法的此项只读证明用于方法替换/类型化能力的已知效果比较,不扩大全局源码检查开关的范围,也不拒绝目前未知的源码实现。只读执行的堆屏障和实际原生目标检查继续沿原VM帧、预算和尾委托作用域传递;它们不能被静态结果替代。可信ObjectPayload读取/比较仍属于宿主约定,不构成Rust隐藏I/O沙箱;只读不证明终止。完整调用图、可变全局/闭包目标和新对象写入的静态证明仍未实现。
|
||||
源码方法的此项只读证明用于方法替换/类型化能力的已知效果比较,不扩大全局源码检查开关的范围,也不拒绝目前未知的源码实现。只读执行的堆屏障和实际原生目标检查继续沿原VM帧、预算和尾委托作用域传递;它们不能被静态结果替代。可信ObjectPayload读取/比较仍属于宿主约定,不构成Rust隐藏I/O沙箱;只读不证明终止。查询另有保守调用图证明,规则见下段;别名/新对象写入、未知调用结果和完整效果推断仍未实现。
|
||||
|
||||
method_contracts.rs新增已证明源码基类的原生效果扩大拒绝/原子性、绑定查询,以及共享局部赋值、自由Cell写入、动态调用保持未知的回归源码。仅编译,不在本地执行。
|
||||
|
||||
|
||||
call_effect 对含 Call 的源码执行只读调用图证明:控制流逐点传播栈上的已知对象/绑定方法事实,分支或循环汇合的不同事实退化为未知。解析当前 globals,缺失时查该函数实际捕获的 builtins;自由Cell从当前闭包读取。绑定接收者及可确定的第一实参用于解析方法/对象字段,按普通属性查找顺序,不绕过实例遮蔽规则。其他参数、调用结果、新建函数/Dict内容和无法解析的调用目标保持未知;不凭注解猜测具体对象。调用图与控制流均采用显式队列,无Rust递归或长期缓存/保活索引,循环关系只证明无写入,不证明终止。
|
||||
|
||||
只有所有可能执行的调用目标都已证明/声明ReadOnly,且源码自身没有未证明操作时,整个源码查询返回ReadOnly。有Mutating/External、Delegated、旧未知原生目标或无法确定的调用就返回None;尤其不将含已知写目标的源码简单总结成Mutating,因为该目标可能重绑定后续全局、Cell或方法,让快照边失效。直接查询原生函数仍返回其签名的已知效果。当前只读证明依赖可信宿主读取/比较和原生ReadOnly契约,不是外部I/O沙箱。
|
||||
|
||||
全局/闭包/方法更新之后每次重新解析,不把某次查询作为永久保证;实际只读调用继续执行原VM作用域和目标检查。method_contracts.rs增加当前builtins与全局覆盖/恢复、自由Cell变化、递归图、控制流事实汇合和实际绑定方法/实例字段更新回归;Tessera增加真实Endor转调、self.helper与重绑定后重新查询且不执行业务方法的回归。仅编译,不本地运行。
|
||||
|
||||
Reference in New Issue
Block a user