Prove read-only effects through current source call graphs

This commit is contained in:
2026-10-22 15:55:44 +08:00
parent cd25f819f2
commit ae821aea2e
11 changed files with 614 additions and 58 deletions
+1 -1
View File
@@ -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)。
+286
View File
@@ -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);
}
+2 -2
View File
@@ -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。
+1
View File
@@ -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
View File
@@ -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,
+274
View File
@@ -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))
}
}
+6 -23
View File
@@ -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
View File
@@ -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 |
+1 -1
View File
@@ -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、异常数据 |
+1 -1
View File
@@ -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 的签名覆盖仍未全部接入。已接入的方法覆写方差与动态名称规则见类型主题。旧能力注册保留数量模式;类型化能力还检查类型/已知效果,并在实际返回和尾委托结果处保留能力返回约束。
+9 -2
View File
@@ -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与重绑定后重新查询且不执行业务方法的回归。仅编译,不本地运行。