From ae821aea2e48544b58cce15e8abd9b2baa567403 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 22 Oct 2026 15:55:44 +0800 Subject: [PATCH] Prove read-only effects through current source call graphs --- crates/core/tests/README.md | 2 +- crates/core/tests/method_contracts.rs | 286 ++++++++++++++++++++++++++ crates/objects/README.md | 4 +- crates/objects/src/README.md | 1 + crates/objects/src/code.rs | 59 +++--- crates/objects/src/effects.rs | 274 ++++++++++++++++++++++++ crates/objects/src/types.rs | 29 +-- docs/README.md | 2 +- docs/runtime/README.md | 2 +- docs/runtime/native-calls.md | 2 +- docs/runtime/types.md | 11 +- 11 files changed, 614 insertions(+), 58 deletions(-) create mode 100644 crates/objects/src/effects.rs diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index 9764b8f..b17575a 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -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)。 diff --git a/crates/core/tests/method_contracts.rs b/crates/core/tests/method_contracts.rs index 1b781af..8269ca4 100644 --- a/crates/core/tests/method_contracts.rs +++ b/crates/core/tests/method_contracts.rs @@ -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); +} diff --git a/crates/objects/README.md b/crates/objects/README.md index b61c347..f43e252 100644 --- a/crates/objects/README.md +++ b/crates/objects/README.md @@ -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。 diff --git a/crates/objects/src/README.md b/crates/objects/src/README.md index f46c230..483f747 100644 --- a/crates/objects/src/README.md +++ b/crates/objects/src/README.md @@ -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 边、语言函数标记 | diff --git a/crates/objects/src/code.rs b/crates/objects/src/code.rs index da7592e..57703fe 100644 --- a/crates/objects/src/code.rs +++ b/crates/objects/src/code.rs @@ -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, diff --git a/crates/objects/src/effects.rs b/crates/objects/src/effects.rs new file mode 100644 index 0000000..6f4bf5c --- /dev/null +++ b/crates/objects/src/effects.rs @@ -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, +} +#[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 { + match self { + Self::Object(object) => Some(object), + _ => None, + } + } +} + +impl ObjectStore { + fn effect_target(&self, mut target: Target) -> Result { + loop { + let payload = self.object(target.function)?.payload(); + let Some(bound) = payload.as_any().downcast_ref::() else { + return Ok(target); + }; + target = Target { + function: bound.function, + receiver: Some(bound.receiver), + }; + } + } + + pub(super) fn infer_call_effect( + &self, + function: ObjectRef, + ) -> Result, VmError> { + let root = self.effect_target(Target { + function, + receiver: None, + })?; + if !self + .object(root.function)? + .payload() + .as_any() + .is::() + { + 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::() + { + 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, + ) -> Result>, VmError> { + let code = self + .payload::(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>> = 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)) + } +} diff --git a/crates/objects/src/types.rs b/crates/objects/src/types.rs index ceaff24..4d64c90 100644 --- a/crates/objects/src/types.rs +++ b/crates/objects/src/types.rs @@ -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, VmError> { - let mut current = function; - loop { - let payload = self.object(current)?.payload(); - if let Some(bound) = payload.as_any().downcast_ref::() { - current = bound.function; - continue; - } - if let Some(source) = payload - .as_any() - .downcast_ref::() - { - return Ok(self - .payload::(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. diff --git a/docs/README.md b/docs/README.md index bf75a2c..734ef25 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/docs/runtime/README.md b/docs/runtime/README.md index 10aa624..5b9225b 100644 --- a/docs/runtime/README.md +++ b/docs/runtime/README.md @@ -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、异常数据 | diff --git a/docs/runtime/native-calls.md b/docs/runtime/native-calls.md index b363f56..7d84edf 100644 --- a/docs/runtime/native-calls.md +++ b/docs/runtime/native-calls.md @@ -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 的签名覆盖仍未全部接入。已接入的方法覆写方差与动态名称规则见类型主题。旧能力注册保留数量模式;类型化能力还检查类型/已知效果,并在实际返回和尾委托结果处保留能力返回约束。 diff --git a/docs/runtime/types.md b/docs/runtime/types.md index 352385a..25c67f3 100644 --- a/docs/runtime/types.md +++ b/docs/runtime/types.md @@ -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与重绑定后重新查询且不执行业务方法的回归。仅编译,不本地运行。