Prove read-only effects through current source call graphs
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user