234 lines
13 KiB
Rust
234 lines
13 KiB
Rust
//! 构造共享 VM 调用序列与同帧循环驱动的字节码。
|
||
//! 用托管函数串联步骤,避免为每个布局节点反复创建独立解释器调用;重复驱动会保留同一个执行帧和预算域。
|
||
//!
|
||
//! mount::synchronize 与 event_dispatch::dispatch 用 build 串起生命周期/事件回调。
|
||
//! source_layout/source_paint 用 repeat_call 循环推进出现位置,并用 collect_and_continue 收集单次源调用结果。
|
||
//! 本模块只生成驱动字节码和托管函数,不在 Rust 中自行解释语句,也不替调用方重置 VM 预算。
|
||
//! 原生调用返回 Invoke 后,VM 在既有帧循环中执行驱动,因此可挂起工作仍有明确的 owning VM。
|
||
use endor_core::{
|
||
CodeLayout, EndorCode, EndorCodeObject, Instruction, NativeContext, ObjectRef, Opcode, Value,
|
||
VmError,
|
||
};
|
||
|
||
/// 将零参数调用列表编成一个托管驱动,按输入顺序执行,返回最后一次调用的值。
|
||
/// 空列表返回 Nil。所有调用都在调用者的 VM 预算内运行,避免宿主逐个调用时重置预算。
|
||
/// name 用于诊断位置;desktop 用来给生成函数挂接桌面 Function 类型。
|
||
/// 调用者是 Mount 生命周期同步和事件路由;返回值是驱动函数身份,不是已经执行完的最后结果。
|
||
pub fn build(
|
||
cx: &mut NativeContext<'_>,
|
||
calls: Vec<ObjectRef>,
|
||
name: &'static str,
|
||
desktop: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 每个函数放入常量池。LoadConst/PushNull/Call 组成零参数调用,
|
||
// 中间结果由 PopTop 丢弃;最后结果留在栈顶交给 ReturnValue。
|
||
// 每个中间调用至多四条指令,额外空间覆盖空序列和 ReturnValue;只是预留容量,不是指令预算。
|
||
let mut instructions = Vec::with_capacity(calls.len() * 4 + 2);
|
||
// 常量池强持有每个 callable,驱动存活期间不依赖调用方的 Rust Vec 保活。
|
||
let mut constants: Vec<Value> = calls.into_iter().map(Value::Object).collect();
|
||
for index in 0..constants.len() {
|
||
// LoadConst 放函数,PushNull 建立 VM Call 所需前缀,Call 0 执行无显式实参的回调。
|
||
instructions.push(Instruction::new(Opcode::LoadConst, index as u32));
|
||
instructions.push(Instruction::simple(Opcode::PushNull));
|
||
instructions.push(Instruction::new(Opcode::Call, 0));
|
||
if index + 1 != constants.len() {
|
||
// 只丢中间结果,避免把最后一个结果也弹掉而让 ReturnValue 面对空栈。
|
||
instructions.push(Instruction::simple(Opcode::PopTop));
|
||
}
|
||
}
|
||
// 没有调用时也必须给 ReturnValue 提供合法栈顶,不能返回一个空栈。
|
||
if constants.is_empty() {
|
||
constants.push(Value::Nil);
|
||
instructions.push(Instruction::new(Opcode::LoadConst, 0));
|
||
}
|
||
instructions.push(Instruction::simple(Opcode::ReturnValue));
|
||
// 编码阶段校验字节码布局,诊断位置使用宿主 name 而不是伪造用户源码行列。
|
||
let code = EndorCode::from_instructions(&instructions, constants, CodeLayout::default())?
|
||
.with_location(name, "<tessera>", 1);
|
||
// NativeContext 暂持代码根,输出函数会通过托管边继续拥有代码与常量。
|
||
let code = cx.allocate(EndorCodeObject::new(code))?;
|
||
// 驱动只读取常量,无需共享用户字典,空私有 globals 不污染 REPL 或主模块。
|
||
let globals = cx.new_dict()?;
|
||
// 函数作为本次原生调用的输出被提升,其余临时拥有根由 NativeContext 退出时释放。
|
||
let driver = cx.new_function(name, code, globals)?;
|
||
crate::ui_bridge::attach_desktop_type(cx, driver, "Function", desktop)?;
|
||
Ok(driver)
|
||
}
|
||
|
||
/// source_layout/source_paint 安装一轮出现遍历时调用,生成固定大小的重复驱动,不按节点数展开字节码。
|
||
/// callback 每次返回 Bool,false 退出;同帧回跳让所有出现和委托源函数共用当前 VM 的执行预算。
|
||
pub fn repeat_call(
|
||
cx: &mut NativeContext<'_>,
|
||
callback: ObjectRef,
|
||
name: &'static str,
|
||
desktop: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 回调返回 Bool;repeat_while 消费这个条件并决定是否跳回本帧开头。
|
||
// 退出后显式返回 Nil,回调的继续条件不会泄露为驱动返回值。
|
||
let mut instructions = vec![
|
||
Instruction::new(Opcode::LoadConst, 0),
|
||
Instruction::simple(Opcode::PushNull),
|
||
Instruction::new(Opcode::Call, 0),
|
||
];
|
||
repeat_while(&mut instructions)?;
|
||
// 条件已由 PopJumpIfFalse 消费;退出时重新装 Nil,不能把最后条件冒充业务返回值。
|
||
instructions.extend([
|
||
Instruction::new(Opcode::LoadConst, 1),
|
||
Instruction::simple(Opcode::ReturnValue),
|
||
]);
|
||
driver(
|
||
cx,
|
||
instructions,
|
||
vec![Value::Object(callback), Value::Nil],
|
||
name,
|
||
desktop,
|
||
)
|
||
}
|
||
|
||
/// source_layout/source_paint 的推进回调调用,为当前出现生成“调用源方法→collector 收结果→返回 true”的助手。
|
||
/// 每次只捕获当前源函数/实参,不预先为全部节点建立助手帧;外层重复驱动再决定下一次出现。
|
||
pub fn collect_and_continue(
|
||
cx: &mut NativeContext<'_>,
|
||
collector: ObjectRef,
|
||
function: ObjectRef,
|
||
arguments: Vec<Value>,
|
||
name: &'static str,
|
||
desktop: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 常量布局固定为 collector、function、各实参、true。
|
||
// 先把 collector 的调用前缀压栈,再执行 function,使其返回值直接成为 collector 的唯一实参。
|
||
// count 同时确定 Call 的显式参数数和 true 常量的池位置,不能把 collector 的一参计入它。
|
||
let count = arguments.len();
|
||
// 先保存两个 callable,再追加业务实参;这份强边关系使源调用挂起期间输入仍可达。
|
||
let mut constants = vec![Value::Object(collector), Value::Object(function)];
|
||
constants.extend(arguments);
|
||
constants.push(Value::Bool(true));
|
||
// 先压 collector 的调用前缀,再压 function 的调用前缀;内部返回值自然成为外部单一实参。
|
||
let mut instructions = vec![
|
||
Instruction::new(Opcode::LoadConst, 0),
|
||
Instruction::simple(Opcode::PushNull),
|
||
Instruction::new(Opcode::LoadConst, 1),
|
||
Instruction::simple(Opcode::PushNull),
|
||
];
|
||
for index in 0..count {
|
||
// 参数常量从 2 开始,0/1 已被 collector/function 占用,保持源方法原始实参顺序。
|
||
instructions.push(Instruction::new(Opcode::LoadConst, index as u32 + 2));
|
||
}
|
||
// 收集器返回值不参与外层调度;丢弃后返回 true,通知重复驱动继续取下一项。
|
||
instructions.extend([
|
||
Instruction::new(Opcode::Call, count as u32),
|
||
Instruction::new(Opcode::Call, 1),
|
||
Instruction::simple(Opcode::PopTop),
|
||
Instruction::new(Opcode::LoadConst, count as u32 + 2),
|
||
Instruction::simple(Opcode::ReturnValue),
|
||
]);
|
||
driver(cx, instructions, constants, name, desktop)
|
||
}
|
||
|
||
/// repeat_call/collect_and_continue 的共同分配助手,把已组装指令与常量变成有宿主 Function 类型的 callable。
|
||
fn driver(
|
||
cx: &mut NativeContext<'_>,
|
||
instructions: Vec<Instruction>,
|
||
constants: Vec<Value>,
|
||
name: &'static str,
|
||
desktop: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 编码常量池与指令后带上宿主诊断位置;字节码有效性由 Aster 的代码构造接口检查。
|
||
let code = EndorCode::from_instructions(&instructions, constants, CodeLayout::default())?
|
||
.with_location(name, "<tessera>", 1);
|
||
// NativeContext 跟踪临时代码根;函数的 trace 边持有代码,不能只靠 Rust ObjectRef 数值保活。
|
||
let code = cx.allocate(EndorCodeObject::new(code))?;
|
||
// 生成驱动不查用户名字,私有 globals 隔离它的空命名环境。
|
||
let globals = cx.new_dict()?;
|
||
// 返回函数根由原生输出机制提升,代码和 globals 的临时根在域结束后可安全清理。
|
||
let function = cx.new_function(name, code, globals)?;
|
||
crate::ui_bridge::attach_desktop_type(cx, function, "Function", desktop)?;
|
||
Ok(function)
|
||
}
|
||
|
||
/// repeat_call 和本模块测试调用,在现有指令尾部追加消费 Bool 条件的分支与回跳。
|
||
/// true 回到本驱动偏移零,false 跳过回跳指令;不新建调用帧,因此不会用递归栈模拟循环。
|
||
pub fn repeat_while(instructions: &mut Vec<Instruction>) -> Result<(), VmError> {
|
||
// 跳转单位是编码后的双字节 code unit,不是 Instruction 数量。
|
||
// 大操作数会生成 EXTENDED_ARG,因此必须编码后计算实际长度。
|
||
let body = Instruction::encode_all(instructions)?.len() / 2;
|
||
// 先用最短零操作数估计回跳宽度,下面迭代直到最终操作数与编码宽度自洽。
|
||
let mut back = Instruction::new(Opcode::JumpBackward, 0);
|
||
// 回跳距离包含自身宽度;而距离变大又可能扩大自身编码宽度。
|
||
// 迭代到宽度/距离一致后,false 跳过回跳指令,true 回到整个驱动的起点。
|
||
loop {
|
||
// units 是当前回跳指令实际占用的 code unit,可能因 EXTENDED_ARG 大于一。
|
||
let units = Instruction::encode_all(&[back])?.len() / 2;
|
||
// 距离覆盖原 body、一个条件分支和回跳自身;目标为开头,不是仅回到上一个 Call。
|
||
let distance = u32::try_from(body + 1 + units)
|
||
.map_err(|_| VmError::ExecutionLimit("layout bytecode"))?;
|
||
if back.argument == distance {
|
||
// 已收敛时 false 跳过 units 个单元,true 落到 JumpBackward 回到开头。
|
||
instructions.push(Instruction::new(Opcode::PopJumpIfFalse, units as u32));
|
||
instructions.push(back);
|
||
return Ok(());
|
||
}
|
||
// 更新操作数后再次编码量宽度;不能只计算一次,跨 255 等边界可能新增 EXTENDED_ARG。
|
||
back.argument = distance;
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use endor_core::{ExecutionLimits, Vm};
|
||
#[test]
|
||
/// Rust 测试框架调用,构造大于单字节跳距的循环,分别验证退出、指令预算及无泄漏清理。
|
||
fn repeated_bytecode_handles_extended_operands_and_keeps_one_frame() {
|
||
// 300 个 Nop 强制跨越单字节跳转距离:false 应正常退出;true 应耗尽指令预算,
|
||
// 而非耗尽帧预算,从而验证循环确实复用同一个执行帧。
|
||
for condition in [false, true] {
|
||
// Nop 扩大 body 编码长度,与实际布局节点数量无关,直接覆盖回跳扩展操作数的边界。
|
||
let mut instructions = vec![Instruction::simple(Opcode::Nop); 300];
|
||
instructions.push(Instruction::new(Opcode::LoadConst, 0));
|
||
repeat_while(&mut instructions).unwrap();
|
||
instructions.extend([
|
||
Instruction::new(Opcode::LoadConst, 1),
|
||
Instruction::simple(Opcode::ReturnValue),
|
||
]);
|
||
// 条件从常量池读取,false 分支跳过回跳后装入 Nil 并正常返回。
|
||
let code = EndorCode::from_instructions(
|
||
&instructions,
|
||
vec![Value::Bool(condition), Value::Nil],
|
||
CodeLayout::default(),
|
||
)
|
||
.unwrap();
|
||
// 每种条件独立 VM,统计回收时不受另一轮遗留对象影响。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 将纯代码值注册到 VM,执行结束后显式释放此拥有根。
|
||
let code = vm.new_code(code).unwrap();
|
||
// 空全局字典满足执行接口,不引入额外用户对象。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 故意只允许一帧:如果实现递归重复而非同帧跳转,会得到错误的帧数耗尽。
|
||
let result = vm.execute_with_limits(
|
||
code,
|
||
globals,
|
||
ExecutionLimits {
|
||
max_frames: 1,
|
||
max_instructions: 1000,
|
||
},
|
||
);
|
||
if condition {
|
||
// true 永不自然退出,应耗尽 instructions 而非 frames,证明循环复用了同一帧。
|
||
assert!(matches!(
|
||
result,
|
||
Err(VmError::ExecutionLimit("instructions"))
|
||
));
|
||
} else {
|
||
// false 只走一次 body 然后返回 Nil,证明分支跳过长度包含完整扩展回跳指令。
|
||
assert_eq!(result.unwrap(), Value::Nil);
|
||
}
|
||
vm.release(code).unwrap();
|
||
vm.release(globals).unwrap();
|
||
// 释放代码与全局字典后执行循环回收,确认生成的常量/函数没有遗留托管对象。
|
||
vm.collect_cycles();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|
||
}
|
||
}
|