From 6d8a5a8fe665ff3800b0a90955e8caffbff90344 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 15 Nov 2026 17:24:34 +0800 Subject: [PATCH] docs(objects): explain executable layout and bytecode verification --- crates/objects/src/code.rs | 130 +++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/crates/objects/src/code.rs b/crates/objects/src/code.rs index e63e1b9..6ffaf0f 100644 --- a/crates/objects/src/code.rs +++ b/crates/objects/src/code.rs @@ -1,3 +1,7 @@ +//! 编译器/宿主提交代码后在此构造不可变执行数据;core::frame 共享代码,eval 按偏移读取指令。 +//! 构造先验证名字布局、解码和控制流栈高度;这不是执行器,也不证明运行时值类型或调用一定成功。 +//! 常量中的对象句柄在 EndorCode 内只是表示,由堆上 EndorCodeObject 的 trace 建立托管强边。 + use std::any::Any; use std::collections::{BTreeSet, VecDeque}; use std::sync::Arc; @@ -6,39 +10,58 @@ use crate::builtins::trace_value; use crate::instruction::{compare_operation, decode, function_counts, invalid}; use crate::{DecodedInstruction, Instruction, ObjectPayload, ObjectRef, Opcode, Value, VmError}; -/// Local slots start with positional parameters. Captured locals occupy their -/// original slots as cells; free-variable cells follow all local slots. +/// 编译器生成、执行帧消费的槽布局:参数占局部槽前缀,捕获局部仍占原槽但存 Cell。 +/// 外部自由变量 Cell 追加在所有局部槽之后,不能与局部变量重名。 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CodeLayout { + /// 位置参数名称顺序,必须与 locals 前缀相同。 pub parameters: Vec, + /// 全部局部名称,参数必须是其完整前缀。 pub locals: Vec, + /// 需要提升为 Cell 的局部名称子集,不额外新增局部槽。 pub cell_variables: Vec, + /// 从外层捕获的 Cell 名称,顺序决定闭包传入槽顺序。 pub free_variables: Vec, + /// 全局与属性操作使用的文本名称索引表。 pub names: Vec, } -/// Source declaration metadata, separate from executable slot layout. +/// 编译器传入的源码类型声明,与执行槽布局分开;类型名称在调用/契约检查时解析。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceSignature { + /// 位置参数的类型注解顺序,数量必须与代码参数槽数相同。 pub parameters: Vec, + /// 非空的返回类型注解名称,尚不保证该名称可成功解析。 pub return_type: String, + /// 可选方法所属类型名,用于源码方法契约;Some 空串非法。 pub method_owner: Option, } +/// 已通过构造校验的 Rust 执行数据,尚未分配为托管代码对象;clone 不为常量对象增加根。 #[derive(Debug, Clone)] pub struct EndorCode { + /// 保留的物理双字节单元编码,含扩展前缀。 bytecode: Vec, + /// 解码后按偏移排序的逻辑指令列表,执行时无需重新组合前缀。 instructions: Vec, + /// 常量表示;Rust clone 不为其中的对象句柄 retain。 constants: Vec, + /// 经 validate_layout 检查的槽和名称布局。 layout: CodeLayout, + /// 从可达控制流计算的最大栈高度,供帧预留容量。 stack_size: usize, + /// 诊断显示的代码名称。 name: String, + /// 诊断显示的源码文件名,不读取该文件。 filename: String, + /// 调用方给出的起始行,供回溯显示,不是逐指令行号表。 first_line: usize, + /// 可选源码声明;不改变已校验的指令布局。 signature: Option, } impl EndorCode { + /// 宿主从原始字节码建立只有参数局部槽的代码;需要额外局部或本地 Cell 时用 with_layout。 pub fn new( bytecode: Vec, constants: Vec, @@ -57,6 +80,7 @@ impl EndorCode { ) } + /// 编译器与宿主把逻辑指令编码后走统一构造校验;此入口不替调用者修补跳转距离。 pub fn from_instructions( instructions: &[Instruction], constants: Vec, @@ -65,13 +89,17 @@ impl EndorCode { Self::with_layout(Instruction::encode_all(instructions)?, constants, layout) } + /// 全部构造入口汇合处:先检查布局,再解码,最后验证索引、跳转和可达路径栈高度。 pub fn with_layout( bytecode: Vec, constants: Vec, layout: CodeLayout, ) -> Result { + // 布局错误必须先拒绝,后续槽索引校验依赖这些不变量。 validate_layout(&layout)?; + // 合并 EXTENDED_ARG 并保留物理单元偏移,未知/截断编码在这里失败。 let instructions = decode(&bytecode)?; + // 校验成功后才建立可供执行器使用的代码,栈高只来自可达指令。 let stack_size = verify(&instructions, constants.len(), &layout)?; Ok(Self { bytecode, @@ -86,7 +114,9 @@ impl EndorCode { }) } + /// 编译器在代码构造后附加声明;这里只检查数量和非空字符串,不解析类型名。 pub fn with_source_signature(mut self, signature: SourceSignature) -> Result { + // 参数注解数量必须对应位置参数;每项与可选 owner 都不允许空串。 if signature.parameters.len() != self.parameters().len() || signature.return_type.is_empty() || signature.parameters.iter().any(String::is_empty) @@ -102,20 +132,20 @@ impl EndorCode { self.signature = Some(signature); Ok(self) } + /// Store 的源码调用/效果/契约检查借用声明;None 表示没有附加源码类型元数据。 pub fn source_signature(&self) -> Option<&SourceSignature> { self.signature.as_ref() } - /// Conservative proof over validated instructions. Local assignment is not - /// automatically pure: shared-reference assignment may update its old value. - /// Unknown calls and writes remain unknown, even when they could be safe for - /// a particular invocation. New opcodes require an explicit audit here. + /// Store/effects 的保守只读证明:所有指令(包括不可达部分)都须在白名单内。 + /// 局部赋值可能触发共享引用更新,不自动视为纯;未知调用/写入也不在这里证明安全。 pub fn is_statically_read_only(&self) -> bool { self.instructions .iter() .all(|decoded| read_only_opcode(decoded.instruction.opcode)) } + /// 编译器/宿主附加诊断位置;不校验文件存在或起始行范围,也不改变执行布局。 pub fn with_location( mut self, name: impl Into, @@ -128,55 +158,67 @@ impl EndorCode { self } + /// 宿主诊断/序列查看借用原编码;不允许绕过验证修改字节。 pub fn bytecode(&self) -> &[u8] { &self.bytecode } + /// eval、效果分析与诊断借用已解码逻辑指令。 pub fn instructions(&self) -> &[DecodedInstruction] { &self.instructions } + /// 执行器读取常量,代码载荷 trace 枚举其对象边;返回切片不新增托管根。 pub fn constants(&self) -> &[Value] { &self.constants } + /// 帧参数绑定与函数默认值校验读取参数顺序。 pub fn parameters(&self) -> &[String] { &self.layout.parameters } + /// 函数闭包校验和帧初始化读取捕获槽顺序。 pub fn free_variables(&self) -> &[String] { &self.layout.free_variables } + /// 编译器/帧与 Store 分析借用完整布局。 pub fn layout(&self) -> &CodeLayout { &self.layout } + /// 执行帧用可达路径最大栈高预留容量,不是执行步数上限。 pub fn stack_size(&self) -> usize { self.stack_size } + /// 错误和代码 repr 读取诊断名称。 pub fn name(&self) -> &str { &self.name } + /// 回溯与 repr 读取文件标识,不访问文件系统。 pub fn filename(&self) -> &str { &self.filename } + /// 回溯/诊断读取调用方设置的起始行。 pub fn first_line(&self) -> usize { self.first_line } + /// 帧创建 localsplus 时计算局部槽加外部自由变量槽的总数。 pub fn slot_count(&self) -> usize { self.layout.locals.len() + self.layout.free_variables.len() } - /// Whether a local/free slot holds a managed closure cell. + /// 帧初始化/闭包访问查询槽是否存 Cell,越界槽返回 false。 pub fn is_cell_slot(&self, slot: usize) -> bool { is_cell_slot(&self.layout, slot) } - /// Name of a local/free slot used by validated instructions. + /// 执行器为已验证槽生成未绑定变量等诊断时查询名称。 /// /// # Panics - /// Panics if the slot is outside this code's local/free-variable layout. + /// slot 超出局部/自由变量布局时 panic;调用者不能传入未经边界检查的槽。 pub fn slot_name(&self, slot: usize) -> &str { + // 局部槽保留原顺序,自由变量索引从局部槽末尾偏移。 if slot < self.layout.locals.len() { &self.layout.locals[slot] } else { &self.layout.free_variables[slot - self.layout.locals.len()] } } - /// Look up a logical instruction by its code-unit offset. + /// eval 按指令指针查询完整逻辑起点;非法偏移返回带位置的字节码错误。 pub fn instruction_at(&self, offset: usize) -> Result { instruction_index(&self.instructions, offset) .map(|index| self.instructions[index]) @@ -189,45 +231,52 @@ impl EndorCode { } } -/// Heap-allocated, immutable and shareable executable. Function objects hold a -/// managed reference to this object rather than embedding a copy of its code. +/// Store::new_code 分配的代码载荷;函数持有其托管引用,帧可共享内部不可变执行数据。 #[derive(Debug, Clone)] pub struct EndorCodeObject { + /// 共享不可变 Rust 执行数据;Arc 本身不是常量对象的托管根。 code: Arc, } impl EndorCodeObject { + /// Store::new_code 将已校验执行数据包装成待分配载荷,共享数据由 Arc 持有。 pub fn new(code: EndorCode) -> Self { Self { code: Arc::new(code), } } + /// Store 校验函数引用的代码布局时借用共享执行数据。 pub fn code(&self) -> &EndorCode { &self.code } - /// Share immutable executable data without copying constants or wordcode. - /// This Rust Arc does not retain heap objects referenced by constants; - /// callers must keep the managed code object rooted while using them. + /// core::frame 共享执行数据,不深拷贝常量和编码;Arc 不是常量托管对象的拥有根。 + /// 调用方使用常量时仍须保持托管代码对象有根,不能只靠此 Arc 延长常量对象寿命。 pub fn shared_code(&self) -> Arc { Arc::clone(&self.code) } } impl ObjectPayload for EndorCodeObject { + /// 不可变代码载荷不实现原地赋值,允许只读局部引用重绑定。 fn reference_assignment_only(&self) -> bool { true } + /// Store::payload 的 Rust 下转型入口。 fn as_any(&self) -> &dyn Any { self } + /// 代码载荷的诊断类别,不是名义类型判断。 fn kind_name(&self) -> &'static str { "code" } + /// Heap 分配/替换代码载荷时登记常量对象边;不递归扫描其指令或源码文本。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + // 仅对象变体贡献边,重复出现的同一对象仍逐次计数。 for value in self.code.constants() { trace_value(value, visit); } } + /// Store::repr 用代码名/文件/起始行生成诊断,不运行代码。 fn repr(&self, _identity: ObjectRef) -> Result { Ok(format!( "", @@ -238,7 +287,8 @@ impl ObjectPayload for EndorCodeObject { } } -// Shared by the local proof and call-graph analysis. Unknown opcodes fail closed. +// 白名单之外一律不在此处证明只读;动态调用图分析可以另行判断已知调用。 +/// 本地只读证明和 effects 调用图分析共享白名单;新增指令需显式审查其副作用。 pub(crate) fn read_only_opcode(opcode: Opcode) -> bool { matches!( opcode, @@ -269,7 +319,9 @@ pub(crate) fn read_only_opcode(opcode: Opcode) -> bool { ) } +/// with_layout 在解码前调用,验证各名称表内部唯一性以及局部/捕获槽的交叉关系。 fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> { + // 每类名称表都检查内部唯一性,不能把全部表混在一个 seen 中。 for names in [ &layout.parameters, &layout.locals, @@ -277,8 +329,11 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> { &layout.free_variables, &layout.names, ] { + // 每张表单独去重;不同表间允许的重名关系在后面单独检查。 let mut seen = BTreeSet::new(); + // 空名称无法可靠绑定,表内重复会使槽或名称索引产生歧义。 for name in names { + // 发现首个违规就退出,构造不产生可执行半成品。 if name.is_empty() || !seen.insert(name) { return Err(VmError::InvalidArgument( "empty or duplicate code variable name", @@ -286,11 +341,13 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> { } } } + // 帧按局部槽前缀绑定参数,不能仅检查包含关系。 if !layout.locals.starts_with(&layout.parameters) { return Err(VmError::InvalidArgument( "parameter names must lead local slots", )); } + // Cell 局部必须来自 locals;外部自由变量不得同时作为本地变量。 if layout .cell_variables .iter() @@ -307,22 +364,29 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> { Ok(()) } +/// 代码公共查询与操作数校验共用:外部自由变量槽全是 Cell,局部槽按名称判断。 fn is_cell_slot(layout: &CodeLayout, slot: usize) -> bool { + // 自由变量段范围内都为 Cell;段外返回 false,避免越界索引。 if slot >= layout.locals.len() { slot < layout.locals.len() + layout.free_variables.len() } else { + // 局部槽按名字判断是否提升为 Cell,不改变其原有位置。 layout.cell_variables.contains(&layout.locals[slot]) } } +/// instruction_at、跳转验证与栈分析用二分查找逻辑起点;前缀中段不是合法目标。 fn instruction_index(instructions: &[DecodedInstruction], offset: usize) -> Option { instructions .binary_search_by_key(&offset, |instruction| instruction.offset) .ok() } +/// DecodedInstruction 与 verify 共用相对偏移计算;此助手只算数,不验证目标边界。 pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result { + // 操作数是代码单元距离,不是字节数或逻辑指令数量。 let argument = instruction.instruction.argument as usize; + // 仅 JumpBackward 做减法,条件跳转与 JumpForward 做加法;checked 防止绕回。 let target = if instruction.instruction.opcode == Opcode::JumpBackward { instruction.next_offset.checked_sub(argument) } else { @@ -331,17 +395,20 @@ pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result Result { + // 操作数检查覆盖全部指令,包括入口不可达部分。 for instruction in instructions { + // 逻辑操作码与完整 u32 参数;前缀已由解码器合并。 let Instruction { opcode, argument } = instruction.instruction; + // 表索引以 usize 使用;其上界由对应布局或常量数量限定。 let index = argument as usize; + // Fast 不能指向 Cell,Deref 必须指向 Cell;跳转必须落在完整逻辑起点。 let valid = match opcode { Opcode::LoadConst => index < constants, Opcode::LoadFast | Opcode::StoreFast | Opcode::DeleteFast => { @@ -368,6 +435,7 @@ fn verify( Opcode::ExtendedArg => false, _ => true, }; + // 将错误定位到指令起点,尚未开始传播栈状态。 if !valid { return Err(invalid( instruction.offset, @@ -375,15 +443,24 @@ fn verify( )); } } + // None 表示尚未从入口到达,不表示其操作数未检查。 let mut heights = vec![None; instructions.len()]; + // decode 保证列表非空;代码入口求值栈为空。 heights[0] = Some(0_usize); + // 工作队列保存待展开的指令索引,每个新到达位置只加入一次。 let mut pending = VecDeque::from([0]); + // 全部可达指令执行前后的最大栈高度。 let mut maximum = 0; + // 前向/回向边共用工作队列,汇合高度一致时不重复展开。 while let Some(index) = pending.pop_front() { + // 队列只保存已赋高度的位置,因此下方 unwrap 由入队不变量保证。 let instruction = instructions[index]; + // 当前指令执行前高度,不含任何类型或值内容信息。 let height = heights[index].unwrap(); + // 逻辑操作码与完整 u32 参数;前缀已由解码器合并。 let Instruction { opcode, argument } = instruction.instruction; let argument = argument as usize; + // required 是最深访问需求,popped/pushed 是净变化来源;Copy/Swap 不弹出其深层项。 let (required, popped, pushed) = match opcode { Opcode::LoadConst | Opcode::LoadFast @@ -407,19 +484,23 @@ fn verify( Opcode::Copy => (argument, 0, 1), Opcode::Swap => (argument, 0, 0), Opcode::Call => { + // 显式参数之外还消费函数与接收者/Null 两项,先检查加法溢出。 let consumed = argument .checked_add(2) .ok_or_else(|| invalid(instruction.offset, "call operand overflow"))?; (consumed, consumed, 1) } Opcode::BuildMap => { + // 每对键值占两项,先检查乘法溢出。 let consumed = argument .checked_mul(2) .ok_or_else(|| invalid(instruction.offset, "map operand overflow"))?; (consumed, consumed, 1) } Opcode::MakeFunction => { + // 函数创建消费默认值、捕获 Cell 和一个代码对象,压回一个函数。 let (defaults, closure) = function_counts(instruction.instruction.argument); + // 两个计数来自 16 位段,总和加代码对象项有界。 let consumed = defaults + closure + 1; (consumed, consumed, 1) } @@ -427,17 +508,21 @@ fn verify( Opcode::ReturnValue => (1, 1, 0), _ => (0, 0, 0), }; + // 在减 popped 前检查足够高度,避免栈下溢。 if height < required { return Err(invalid(instruction.offset, "evaluation stack underflow")); } + // 返回必须恰好留下一个结果供移出,禁止帧内遗留其他栈项。 if opcode == Opcode::ReturnValue && height != 1 { return Err(invalid( instruction.offset, "RETURN_VALUE must leave an empty frame stack", )); } + // 校验保证减法安全;随后统计当前和后继高度。 let next_height = height - popped + pushed; maximum = maximum.max(height).max(next_height); + // 返回无后继;条件跳转同时传播跳转/顺序两条路径,不猜测条件值。 let successors: Vec = match opcode { Opcode::ReturnValue => Vec::new(), Opcode::JumpForward | Opcode::JumpBackward => vec![jump_target(instruction)?], @@ -446,11 +531,15 @@ fn verify( } _ => vec![instruction.next_offset], }; + // 每条后继都必须指向现存逻辑指令,顺序落到代码末尾也属于错误。 for successor in successors { + // 将物理单元偏移转换为逻辑指令索引,拒绝扩展前缀中部。 let target = instruction_index(instructions, successor).ok_or_else(|| { invalid(instruction.offset, "execution falls off the code object") })?; + // 已到达位置只能接受同高;新位置设置高度并排队。 match heights[target] { + // 回边或分支汇合必须与第一次到达时的高度完全一致。 Some(existing) if existing != next_height => { return Err(invalid( successor, @@ -458,6 +547,7 @@ fn verify( )); } Some(_) => {} + // 首次到达的后继写入高度,之后排队检查它的栈需求。 None => { heights[target] = Some(next_height); pending.push_back(target);