Files
endorphin-tessera-gui/src/runtime/vm_sequence.rs
T

234 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 构造共享 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);
}
}
}