docs(objects): explain snapshot call effect analysis
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
//! Store::call_effect 的当前快照证明;None 表示无法证明,不代表确定有副作用。
|
||||
//! 查询经过 Store 读取追踪,但不执行源码或分配托管对象;所有身份仅借用。
|
||||
//! 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};
|
||||
@@ -14,18 +16,24 @@ struct Target {
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Fact {
|
||||
// 没有可证明的身份,不是实际 Nil。
|
||||
Unknown,
|
||||
// 调用协议 Null 标记,不是语言空值。
|
||||
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),
|
||||
@@ -35,9 +43,13 @@ impl Fact {
|
||||
}
|
||||
|
||||
impl ObjectStore {
|
||||
// 解包 BoundMethod,保留底层函数和实际接收者,供调用图节点归一化。
|
||||
fn effect_target(&self, mut target: Target) -> Result<Target, VmError> {
|
||||
// 逐层解绑定载荷,不递归进入 Rust 调用栈。
|
||||
loop {
|
||||
// 验证函数身份并记录当前载荷读取,避免绕过依赖追踪。
|
||||
let payload = self.object(target.function)?.payload();
|
||||
// 不是绑定方法即结束归一化,不更改现有接收者。
|
||||
let Some(bound) = payload.as_any().downcast_ref::<BoundMethod>() else {
|
||||
return Ok(target);
|
||||
};
|
||||
@@ -48,14 +60,17 @@ impl ObjectStore {
|
||||
}
|
||||
}
|
||||
|
||||
// types::call_effect 委托入口;原生根返回已声明效果,源码根只尝试证明全图只读。
|
||||
pub(super) fn infer_call_effect(
|
||||
&self,
|
||||
function: ObjectRef,
|
||||
) -> Result<Option<CallEffect>, VmError> {
|
||||
// 根也可能是绑定方法,先解包再识别源码/原生。
|
||||
let root = self.effect_target(Target {
|
||||
function,
|
||||
receiver: None,
|
||||
})?;
|
||||
// 非源码根查询原生签名,Delegated 效果动态决定,不能直接当作整体效果。
|
||||
if !self
|
||||
.object(root.function)?
|
||||
.payload()
|
||||
@@ -66,19 +81,26 @@ impl ObjectStore {
|
||||
(signature.effect != CallEffect::Delegated).then_some(signature.effect)
|
||||
}));
|
||||
}
|
||||
// 迭代工作队列防止递归调用图造成 Rust 递归。
|
||||
let mut pending = VecDeque::from([root]);
|
||||
// 节点去重包含接收者,同一函数的不同绑定对象不能混为一项。
|
||||
let mut visited = BTreeSet::new();
|
||||
// 所有可达节点都能证明只读才允许根返回 ReadOnly。
|
||||
while let Some(target) = pending.pop_front() {
|
||||
// 子调用也可能经过绑定方法,统一归一化。
|
||||
let target = self.effect_target(target)?;
|
||||
// 递归回边已访问就跳过,不重复展开。
|
||||
if !visited.insert(target) {
|
||||
continue;
|
||||
}
|
||||
// 源码节点分析栈事实,原生节点则必须明确声明 ReadOnly。
|
||||
if let Some(source) = self
|
||||
.object(target.function)?
|
||||
.payload()
|
||||
.as_any()
|
||||
.downcast_ref::<EndorFunctionObject>()
|
||||
{
|
||||
// 任意一个节点无法证明,整个源码根的证明立即返回 None。
|
||||
let Some(callees) = self.read_only_callees(source, target.receiver)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -87,41 +109,55 @@ impl ObjectStore {
|
||||
.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.
|
||||
// 未知/非只读目标可重绑定后续全局、Cell 或方法,当前快照不足以证明整体效果。
|
||||
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()));
|
||||
}
|
||||
// 有 Call 时要求其他全部指令都通过本地白名单。
|
||||
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);
|
||||
}
|
||||
// 每个逻辑指令入口一份抽象栈,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");
|
||||
// 仅保留调用目标识别所需事实,结果通常退化为 Unknown。
|
||||
match opcode {
|
||||
Opcode::Await => return Ok(None),
|
||||
Opcode::LoadConst => stack.push(Fact::value(code.constants()[argument].clone())),
|
||||
@@ -131,8 +167,11 @@ impl ObjectStore {
|
||||
Fact::Unknown
|
||||
}),
|
||||
Opcode::LoadGlobal => {
|
||||
// 与 eval 一致,先查询全局字典,再回退捕获的内置空间。
|
||||
let name = &code.layout().names[argument];
|
||||
// 名称读取登记命中/缺失依赖,后续重绑定能影响快照证明。
|
||||
let mut value = self.dict_get_name(function.globals(), name)?;
|
||||
// 只有缺键才回退,Some(Nil) 不等于名称不存在。
|
||||
if value.is_none()
|
||||
&& let Some(builtins) = function.builtins()
|
||||
{
|
||||
@@ -141,16 +180,20 @@ impl ObjectStore {
|
||||
stack.push(value.map(Fact::value).unwrap_or(Fact::Unknown));
|
||||
}
|
||||
Opcode::LoadDeref | Opcode::LoadClosure => {
|
||||
// 外部自由变量可读当前捕获 Cell;局部只尝试识别已知接收者。
|
||||
let fact = if argument >= code.layout().locals.len() {
|
||||
// 把 localsplus 自由变量段索引换算到函数 closure 列表。
|
||||
let cell = *function
|
||||
.closure()
|
||||
.get(argument - code.layout().locals.len())
|
||||
.ok_or(VmError::InvalidArgument(
|
||||
"function closure does not match code",
|
||||
))?;
|
||||
// LoadClosure 保留 Cell 自身身份,LoadDeref 读取其当前内容。
|
||||
if opcode == Opcode::LoadClosure {
|
||||
Fact::Object(cell)
|
||||
} else {
|
||||
// 空 Cell 为 Unknown,其他协议/句柄错误仍传播。
|
||||
match self.cell_get(cell) {
|
||||
Ok(value) => Fact::value(value),
|
||||
Err(VmError::EmptyCell) => Fact::Unknown,
|
||||
@@ -167,6 +210,7 @@ impl ObjectStore {
|
||||
Opcode::PushNull => stack.push(Fact::Null),
|
||||
Opcode::Copy => stack.push(stack[stack.len() - argument]),
|
||||
Opcode::Swap => {
|
||||
// Swap 深度已由代码验证,直接模拟事实位置交换。
|
||||
let length = stack.len();
|
||||
stack.swap(length - 1, length - argument);
|
||||
}
|
||||
@@ -184,8 +228,11 @@ impl ObjectStore {
|
||||
*stack.last_mut().unwrap() = Fact::Unknown;
|
||||
}
|
||||
Opcode::LoadAttr => {
|
||||
// 只有 owner 身份已知才可查询其属性,未知 owner 不猜测方法。
|
||||
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 }) => {
|
||||
@@ -205,7 +252,9 @@ impl ObjectStore {
|
||||
};
|
||||
}
|
||||
Opcode::Call => {
|
||||
// 调用输入包括函数、接收者或 Null 及显式参数,代码校验保证足够高度。
|
||||
let start = stack.len() - argument - 2;
|
||||
// Null 协议从首显式实参识别可能接收者,否则用隐式接收者项。
|
||||
let implicit = if stack[start + 1] == Fact::Null {
|
||||
(argument > 0)
|
||||
.then(|| stack[start + 2])
|
||||
@@ -213,6 +262,7 @@ impl ObjectStore {
|
||||
} else {
|
||||
stack[start + 1].object()
|
||||
};
|
||||
// 未知函数目标无法证明整个函数;已知方法直接保留绑定接收者。
|
||||
let target = match stack[start] {
|
||||
Fact::Object(function) => Target {
|
||||
function,
|
||||
@@ -221,6 +271,7 @@ impl ObjectStore {
|
||||
Fact::Method(target) => target,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
// 记录目标后移除输入,调用结果不猜测身份,标为 Unknown。
|
||||
callees.insert(target);
|
||||
stack.truncate(start);
|
||||
stack.push(Fact::Unknown);
|
||||
@@ -230,14 +281,17 @@ impl ObjectStore {
|
||||
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 => {}
|
||||
// 注意:当前白名单含算术操作,但此模拟器尚无对应分支;含 Call 的混合路径需单独修复。
|
||||
_ => unreachable!("unproved opcodes were rejected before analysis"),
|
||||
}
|
||||
// 条件跳转传播两条后继,返回无后继,不根据当前值猜测条件真假。
|
||||
let successors = match opcode {
|
||||
Opcode::ReturnValue => vec![],
|
||||
Opcode::JumpForward | Opcode::JumpBackward => {
|
||||
@@ -248,13 +302,19 @@ impl ObjectStore {
|
||||
}
|
||||
_ => vec![decoded.next_offset],
|
||||
};
|
||||
// 后继都已通过代码验证,必须落在完整逻辑指令起点。
|
||||
for offset in successors {
|
||||
// 将物理单元偏移查为逻辑索引,expect 依赖已验证控制流。
|
||||
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;
|
||||
// Unknown 不恢复为具体事实,相同身份继续保留。
|
||||
for (old, new) in existing.iter_mut().zip(&stack) {
|
||||
// 不同路径的目标不能任取其一,合并为 Unknown。
|
||||
if *old != *new && *old != Fact::Unknown {
|
||||
*old = Fact::Unknown;
|
||||
changed = true;
|
||||
@@ -265,6 +325,7 @@ impl ObjectStore {
|
||||
states[next] = Some(stack.clone());
|
||||
true
|
||||
};
|
||||
// 状态更保守时重新处理后继,直至所有入口稳定。
|
||||
if changed {
|
||||
work.push_back(next);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user