docs(objects): explain snapshot call effect analysis

This commit is contained in:
2026-11-15 17:33:03 +08:00
parent c25cb6756e
commit 58ec874dc0
+63 -2
View File
@@ -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);
}