//! 把 Aster 编译/执行与 Tessera 展示生命周期连接起来。Candidate 拥有 VM 与宿主根,ReplSession 保留实际运行中的全局环境。 //! 完整源码 PUT 使用隔离候选;Console 输入则在已有 VM 上执行,先前发生的写入不等于可以回放或事务回滚。 //! 刷新状态机遍历窗口并逐步推进布局/绘制。完成所有必要轮次才形成文档;取消会释放挂起调用并恢复未提交的 Mount。 //! //! api::worker 为完整 PUT 创建 ReplSession 候选,为 Console/事件/resize 调用已有会话;只有该 worker 拥有 VM。 //! UI 类型与刷新由 bridge 适配;此模块负责源码预算、真实会话、执行结果和挂起刷新的生命周期。 //! compile/evaluate 是一次性入口,关闭 Candidate 后只返回纯数据;生产持久路径使用增量入口并保存续体。 //! 本模块仍直接依赖 UI Frame/桥接,不是已经可以独立搬走的通用 Nexus runtime,迁移需先抽出宿主接口。 //! //! 增量刷新有两层“继续”:ReplSession 保存全窗口 RefreshWork,bridge 保存当前窗口的布局或绘制续体。 //! API worker 下一量子恢复前者,step_refresh 再选择同一窗口恢复后者;两层都不重放已经执行的 Console/事件副作用。 //! 尺寸目标确实变化才由 retarget_refresh 取消当前视觉续体并重启稳定轮;取消派生几何不会撤销已发生的业务写入。 use anyhow::{Result, bail}; use endor_core::{ExecutionLimits, ObjectRef, Value, Vm, VmError}; use frontend::{ EndorReader, SourceUnit, Span, ast::{AstRoot, Decl, DeclKind, Expr, ExprKind, Spanned, Stmt, StmtKind, visitor::Visitor}, }; use std::collections::BTreeSet; use crate::{ document::{CodeNode, RenderDocument}, endor_script::{self, SourcePlan}, layout, ui_bridge, }; /// API、完整加载、Console 与 evaluate 共用的原始输入字节预算;不是字符数或 AST 节点数量。 pub const MAX_SOURCE_BYTES: usize = 1_048_576; // 每次源执行的硬上限;脚本多片段会分摊指令额度,不能每段重新获得完整额度。 const LIMITS: ExecutionLimits = ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }; // AstBudget 访问者的结构预算,在加载代码前拒绝过大或过深语法树。 const MAX_AST_NODES: usize = 10_000; const MAX_AST_DEPTH: usize = 64; /// 一次性编译/现有 API 检查路径调用,在临时 VM 执行并提取文档,不返回持久可响应事件的 ReplSession。 pub fn compile(source: &str, namespace: &str, submission: u64) -> Result { if source.len() > MAX_SOURCE_BYTES { // 先限制输入再进行词法/AST 分配,避免仅靠执行预算保护不了源码解析开销。 bail!("source exceeds the 1 MiB limit"); } // 把请求来源和提交编号放入诊断 SourceUnit,后续注解错误可显示真实源行列。 let source = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source); // Module/Script 组织由宿主确定,语法解析和位置映射继续委托 endor_script/Aster。 let plan = endor_script::parse(&source)?; // 各 Script 根共享 AST 计数预算,不能通过拆成很多交互片段绕过整份源码节点上限。 let mut budget = AstBudget::default(); for ast in plan.roots() { // 同一个 budget 累加每段,不因片段边界重置节点/深度检查。 budget.visit_ast_root(ast)?; } // 编译得到可加载模板,此时还没有把业务语句在任何 VM 中运行。 let compiled = compile_plan(&source, &plan)?; // 纯数据代码树供 Sources 浏览,和下面实际可执行代码/托管图不是同一结构。 let root = architecture(&source, &plan, namespace, submission); // Candidate 是此一次性域的 VM 与根拥有者,所有出口都在下面 close 收尾。 let mut candidate = Candidate::new()?; // 保存整个执行结果,不能让内部 ? 跳过 Candidate::close。 let result: Result = (|| { // 同一程序的所有段共享一个 globals,真实引用由 candidate.roots 持有。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); // 类型安装在执行前完成,全局别名引用同一批名义对象,不在每片脚本重建 UI 类型。 let bridge = ui_bridge::install(&mut candidate.vm, &mut candidate.roots)?; bridge.install_globals(&mut candidate.vm, globals)?; // Script 多段分别执行,指令总额按片段数均分;Module 作为整体获得完整预算,避免每段都重获一百万。 let limits = match &plan { SourcePlan::Module(_) => LIMITS, SourcePlan::Script(_) => ExecutionLimits { max_instructions: LIMITS.max_instructions / compiled.len().max(1), ..LIMITS }, }; for template in &compiled { // 每段加载后有一个代码拥有根,统一登记供候选失败/完成时释放。 let code = template.load(&mut candidate.vm)?; candidate.roots.push(code); // 顶层语句返回值此处不作为默认展示根,脚本应显式 render;返回拥有值需立即释放。 let value = candidate .vm .execute_with_limits(code, globals, limits) .map_err(|error| runtime_error(&candidate.vm, error))?; candidate.vm.release_value(value)?; } bridge.refresh_bindings(&mut candidate.vm)?; // 源业务执行后刷新绑定,再依据脚本显式页面或模块 main 返回结果选视觉树。 let ui_root = match &plan { // 脚本没有约定 main 返回值,必须有源 API 安排的页面。 SourcePlan::Script(_) => bridge .rendered(&candidate.vm)? .ok_or_else(|| anyhow::anyhow!("script must call ui.page().render(root)"))?, // 模块先执行定义再调用 main;若 main 未显式 render,可将其返回值转换成展示根。 SourcePlan::Module(_) => { // 从真实全局字典找 main,不能通过 AST 名字存在就假定运行结果也是 callable。 let entry = candidate .vm .dict_get(globals, &Value::Text("main".into()))? .ok_or_else(|| { anyhow::anyhow!("declare main or render a page from a top-level script") })?; // 对象身份是可调用检查的第一步,VM.call 仍继续验证具体 callable 契约。 let Value::Object(entry) = entry else { bail!("main must be a callable function"); }; // main 返回值有局部拥有根,快照成功/失败都须经过下面 release_value。 let value = candidate .vm .call_with_limits(entry, &[], LIMITS) .map_err(|error| runtime_error(&candidate.vm, error))?; bridge.refresh_bindings(&mut candidate.vm)?; // 尊重 main 已主动展示的页面,否则用返回 Block 直接提取兼容快照。 let snapshot = bridge .rendered(&candidate.vm) .and_then(|rendered| match rendered { Some(root) => Ok(root), None => ui_bridge::snapshot(&candidate.vm, &value), }); // 提取快照结果先暂存,成功或失败都释放 main 返回值的拥有根,再传播快照错误。 candidate.vm.release_value(value)?; snapshot? } }; // 输出文档只持源码/代码树/不可变视觉与几何,没有 ObjectRef 或可执行 VM 指针。 let document = RenderDocument { windows: std::collections::BTreeMap::new(), source: source.text().to_owned(), root: Some(root), frame: layout::frame(&ui_root)?, ui_root: Some(ui_root), }; document.validate()?; Ok(document) })(); // 独立构建在成功与错误路径都显式释放宿主根/回收堆;返回成功文档也要求 cleanup 成功。 let cleanup = candidate.close(); match (result, cleanup) { (Ok(document), Ok(())) => Ok(document), (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error), (Err(error), Err(cleanup)) => Err(error.context(format!("candidate cleanup: {cleanup}"))), } } /// api::worker 的 evaluate 请求调用,在一次性无 UI 全局的隔离 VM 求单表达式,返回可显示字符串。 pub fn evaluate(source: &str) -> Result { if source.len() > MAX_SOURCE_BYTES { bail!("expression exceeds the 1 MiB limit"); } // 独立来源名明确区别于持久 Console transcript,不读取其历史或对象堆。 let source = EndorReader::from_text("console expression", source); // 隔离 evaluate 只接受一个表达式,默认编译选项不安装 ui 桥接,也不读已发布程序的 globals。 let ast = frontend::parse_source_ast(&source, frontend::ParseMode::Expression)?; AstBudget::default().visit_ast_root(&ast)?; // 默认全局只有语言内置,不为 evaluate 暗中安装 ui 或活程序用户全局。 let compiled = compiler::compile_ast(&ast, &source, &compiler::CompileOptions::default()) .map_err(|error| source.annotate(error))?; // 独立候选统一持有执行根,表达式失败也由 close 释放整个域。 let mut candidate = Candidate::new()?; // 暂存执行/格式化错误,保证外层 close 不因这里的 ? 被跳过。 let result = (|| { // 表达式全局环境为空的新字典,不能用运行时 namespace 文档代替真实 globals。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); // 模板加载到此 VM 后才有真实代码对象,拥有根记入 Candidate。 let code = compiled.load(&mut candidate.vm)?; candidate.roots.push(code); // 一次有界执行产生拥有返回值,字符串化后显式释放。 let value = candidate .vm .execute_with_limits(code, globals, LIMITS) .map_err(|error| runtime_error(&candidate.vm, error))?; // 只把标量或对象 repr 字符串交回 API,真实 Value 随一次性 VM 清理,不跨线程交付托管引用。 let rendered = match &value { Value::Nil => "nil".to_owned(), Value::Bool(v) => v.to_string(), Value::Text(v) => format!("{v:?}"), Value::Number(v) => v.to_string(), Value::Object(v) => candidate.vm.repr(*v)?, }; candidate.vm.release_value(value)?; Ok(rendered) })(); // 先尝试关闭候选,再合并执行和清理结果,不能只因表达式已有字符串结果就跳过清理。 let cleanup = candidate.close(); match (result, cleanup) { (Ok(value), Ok(())) => Ok(value), (Err(e), _) | (_, Err(e)) => Err(e), } } /// compile/prepare_program 调用,建立 UI 宿主全局名集合,再复用增量编译助手生成模板序列。 fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result> { compile_plan_with_globals( source, plan, std::iter::once("ui") .chain(ui_bridge::TYPE_NAMES.iter().copied()) .chain(ui_bridge::QUERY_NAMES.iter().copied()) .map(str::to_owned) .collect(), ) } /// compile_plan 与 execute_mode 调用,按源序编译 Script 各段,同时支持完整模块和声明前向引用。 fn compile_plan_with_globals( source: &SourceUnit, plan: &SourcePlan, mut globals: BTreeSet, ) -> Result> { // 先收集全部函数/类声明名以支持前向引用;普通绑定仍按源码执行次序逐段加入已知全局名。 for root in plan.roots() { if let AstRoot::Interactive(input) = root { // 完整模块的声明可见性由 Aster 自己处理;只有拆成多个交互根时宿主需预收集声明名。 for statement in &input.statements { if let StmtKind::Declaration(decl) = &statement.kind { // 只预声明函数/类,不提前把任意赋值名当作已存在业务值。 let name = match &decl.kind { DeclKind::Function(function) => &function.name, DeclKind::Class(class) => &class.name, }; globals.insert(name.text.clone()); } } } } // 每个根保留一个模板,执行顺序必须与原根切片一一对应,不能按名字排序。 let mut compiled = Vec::with_capacity(plan.roots().len()); /// 本函数在每段成功编译后调用,递归收集后续段可能引用的绑定名,不执行或预测条件真值。 fn assigned(statement: &Stmt, globals: &mut BTreeSet) { match &statement.kind { StmtKind::Binding(b) => { // 绑定给后续片段的静态名称解析提供名字;字典中的真实值仍在执行时建立。 globals.insert(b.name.text.clone()); } StmtKind::Assignment(a) => { // 顶层赋值也可能引入名字,按 Aster 交互语义让后续段可以编译引用。 globals.insert(a.name.text.clone()); } StmtKind::If(b) => { // 两个分支都收集“可能声明”名字,不在编译阶段决定哪条路径会执行。 for s in &b.body.statements { assigned(s, globals); } if let Some(other) = &b.otherwise { // else 中的潜在绑定也要让后续片段可编译引用,但这里不预先在 globals 字典创建实际值。 for s in &other.statements { assigned(s, globals); } } } StmtKind::While(b) => { // 循环体可能写入全局;是否真正执行一次由 VM 决定,宿主不展开循环。 for s in &b.body.statements { assigned(s, globals); } } // 表达式、return、成员赋值等不引入新的普通全局名;函数内部作用域由语言编译器处理。 _ => {} } } for root in plan.roots() { // 每段得到当前已知全局集合快照,先编译再把本段绑定加入下一段上下文。 let options = compiler::CompileOptions { globals: globals.iter().cloned().collect(), }; compiled.push( compiler::compile_ast(root, source, &options) .map_err(|error| source.annotate(error))?, ); // 每段编译成功才把绑定/赋值名字加入下一段编译上下文,语法与代码生成都继续委托 Aster。 if let AstRoot::Interactive(input) = root { for statement in &input.statements { assigned(statement, &mut globals); } } } Ok(compiled) } /// refresh_step 挂起 paint 时创建、下一轮恢复/取消时消费,文档草稿与所属窗口续体必须成对保留。 struct PendingPaint { // 原窗口 ID,恢复/取消前显式 select_window,不能依赖稍后变化的 current。 window: u64, // 对应本次完成布局的源码/视觉/几何草稿,paint 结束前不得对外发布。 document: RenderDocument, // owning VM 中未完成的 source_paint Work,取消时显式关闭,Rust drop 本身不替代 VM 清理。 work: Box, } /// 跨 worker 轮次的全窗口刷新状态,保存已完成草稿和当前布局/绘制,直到稳定再发布。 struct RefreshWork { // 已完成窗口的草稿先留在本轮;所有窗口收敛后才合成一次完整发布。 // RefreshWork 先克隆最近完整文档作为草稿基线,各窗口更新暂存于此;全轮稳定前不写共享 NamespaceState。 documents: std::collections::BTreeMap, // 当前稳定性遍历轮剩余窗口,按 bridge 的退旧优先次序排队。 windows: std::collections::VecDeque, // 本片正在推进窗口,pending/paint 的上下文必须与其一致。 current: Option, // 当前帧不可变根草稿,布局完成再绘制仍使用这一轮同一份快照。 root: Option>, // 续体必须保留其 owning VM;取消时显式关闭,不能靠丢弃 Rust 值。 pending: Option>, // 与 pending 布局阶段互斥的未完成绘制,完成后才把当前窗口文档写进 documents。 paint: Option>, // pass_changed 要求再跑一轮确认稳定,changed 记录整次刷新是否有可发布变化。 pass_start: bool, // 一轮是否有变化,决定下一轮还需确认稳定;不等于最终对外是否已有变化。 pass_changed: bool, // 整次刷新累计变化,用于稳定后决定返回文档还是 None。 changed: bool, // 整次刷新工作量预算,跨多个 worker 量子累计,不能让每片各得一份完整收敛额度。 work: usize, // 记录本轮尺寸目标,下一次 worker 轮次据此判断是否需要重定向。 viewports: Option>, } /// 刷新仍在进行或已完成;完成也可能没有新文档需要发布。 enum RefreshProgress { // worker 保留整个 ReplSession,下次继续已有布局/绘制续体,不接新业务写入。 Pending, // 已稳定;None 为没有新文档,Some 才有可以让 worker 校验/发布的完整值。 Complete(Option>), } /// 持久源运行时与其桥接会话,保存实际全局对象、代码投影、全窗口文档和挂起刷新。 pub struct ReplSession { // 真正 VM、全局拥有根和清理状态,和 HTTP/UI 读取的文档快照分离。 candidate: Candidate, // 同 VM 的 Tessera 扩展能力,实际窗口/布局/绘制规则留在 bridge/model/render。 bridge: ui_bridge::UiBridge, // worker 分配的运行时代际 ID,输入即使数值窗口/Mount 相同也不能跨代沿用。 pub id: u64, // 已接受 Console 输入数量与累计 AST 节点数,完整源加载和后续输入都受各自预算控制。 inputs: usize, ast_nodes: usize, // 完整程序或累加后的 Console transcript,供发布和 Sources 使用,不通过回放恢复执行状态。 source: String, // 交互记录的纯数据代码树,与 Candidate 的真实 globals/函数引用不是同一存储。 nodes: Vec, // 回调/渲染失败后置位,拒绝继续普通业务输入;保留画面不表示堆还能安全执行。 faulted: bool, // 完整程序的 Sources 根,首次 Console 追加时移入 nodes 保留原程序浏览记录。 program_root: Option, // 最近完成的逐窗基线文档;未稳定刷新只改 RefreshWork 草稿,不提前替换共享发布状态。 documents: std::collections::BTreeMap, // 同一个 VM 上至多一份跨片刷新,所有业务写入口先 require_idle_refresh。 refresh: Option, } /// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。 pub struct ReplResult { // 用于 Console/API 的字符串结果,不跨线程暴露拥有根 Value。 pub value: String, // 只有本轮已经稳定完成刷新才返回完整文档,挂起或无变化时为 None。 pub document: Option, // 当前 globals 条目数的显示统计,具体计算按 execute_mode 的接口约定处理。 pub bindings: usize, // 已接受输入编号,供 API/Console 对应 transcript 与代码树记录。 pub input: usize, } impl ReplSession { /// Compile once into an isolated candidate, then keep its actual heap and /// callback environments alive after the first successful publication. #[cfg(test)] /// 测试用同步装载入口:建立完整候选并排空初始刷新,返回真实运行时与完成文档。 pub fn load( source: &str, namespace: &str, submission: u64, id: u64, ) -> Result<(Self, RenderDocument)> { Self::load_with_viewport(source, namespace, submission, id, None) } #[cfg(test)] /// 测试用同步装载入口:建立完整候选并排空初始刷新,返回真实运行时与完成文档。 pub fn load_with_viewport( source: &str, namespace: &str, submission: u64, id: u64, viewport: Option, ) -> Result<(Self, RenderDocument)> { // 测试只接受主窗口尺寸,先建立真实 VM,再同步排空初始刷新便于直接断言帧内容。 let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?; // 成功执行但没发布页面不算完整 GUI 程序,避免返回一个表面“成功”的空候选。 let document = runtime .refresh_document()? .ok_or_else(|| anyhow::anyhow!("program did not publish a Page"))?; runtime .bridge .reclaim(&mut runtime.candidate.vm, &mut runtime.candidate.roots)?; Ok((runtime, document)) } /// api::worker 的 PUT 完整源码路径调用,建立候选并只推进有限刷新,未稳定时保留 runtime 等后续量子。 pub fn load_incremental( source: &str, namespace: &str, submission: u64, id: u64, viewport: Option, ) -> Result<(Self, Option)> { // 源程序执行在隔离 Candidate,旧活跃程序和 GUI 发布文档尚未替换。 let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?; // 首轮只提供主窗口已知尺寸,源程序新建的其他窗口由后续 resize 通知补充。 let viewports = viewport .map(|size| (crate::window::MAIN_WINDOW, size)) .into_iter() .collect(); // Option 文档为 None 既可能挂起也可能无变化,必须再看 has_pending_refresh 区分。 let document = runtime.refresh_after_mutation(Some(&viewports))?; // 初始刷新若已完成必须确实有 Page 文档才可接受候选;仍挂起则把整个运行时交 worker 下一量子继续。 if !runtime.has_pending_refresh() { anyhow::ensure!(document.is_some(), "program did not publish a Page"); runtime .bridge .reclaim(&mut runtime.candidate.vm, &mut runtime.candidate.roots)?; } Ok((runtime, document)) } /// load/load_incremental 的共同准备入口,在新 VM 执行完整源码,验证已安排页面;布局/发布留给外层。 fn prepare_program( source: &str, namespace: &str, submission: u64, id: u64, viewport: Option, ) -> Result { if source.len() > MAX_SOURCE_BYTES { // 完整 PUT 候选在创建 VM 前限制原始 UTF-8 字节数,解析/代码生成尚未开始。 bail!("source exceeds the 1 MiB limit"); } // 来源标注把 API namespace/提交号接入 Aster 错误来源,不改变用户原文。 let unit = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source); // 按 Module/Script 路径编译,脚本片段仍共享同一个 globals。 let plan = endor_script::parse(&unit)?; // 一份输入的所有根累计使用同一结构预算,过大 AST 在业务执行前失败。 let mut budget = AstBudget::default(); for ast in plan.roots() { budget.visit_ast_root(ast)?; } // 全部编译成功后才创建并执行新运行时,解析/编译失败不改变任何活程序对象。 let compiled = compile_plan(&unit, &plan)?; // new 安装类型和 globals,但不主动打开页面;源程序决定要展示的内容。 let mut runtime = Self::new(id)?; runtime .bridge .set_viewport(&mut runtime.candidate.vm, viewport)?; // 完整程序代码在候选自己的真实 globals 中执行,之后保留该堆用于事件与 Console,不通过重放历史恢复状态。 let globals = runtime.candidate.globals.expect("program globals"); // 根据已编译根数确定脚本每段硬额度;完整模块仍使用一份 LIMITS 执行模块定义。 let limits = if matches!(plan, SourcePlan::Script(_)) { // 指令硬额度在脚本段间均分,不因一个输入拆成更多交互段而放大执行总量。 ExecutionLimits { max_instructions: LIMITS.max_instructions / compiled.len().max(1), ..LIMITS } } else { // 模块作为一个编译单元执行定义,之后 main 有单独一次有界调用。 LIMITS }; for template in &compiled { // 代码只需此段执行期间持有,执行结果暂存,代码根释放后再传播调用错误。 let code = template.load(&mut runtime.candidate.vm)?; // 保存 Result 以保证失败执行也经过 vm.release(code)。 let result = runtime .candidate .vm .execute_with_limits(code, globals, limits) .map_err(|error| runtime_error(&runtime.candidate.vm, error)); runtime.candidate.vm.release(code)?; runtime.candidate.vm.release_value(result?)?; } if matches!(plan, SourcePlan::Module(_)) { // 模块顶层只安装定义,main 从实际 globals 查找,而非只依据 AST 声明列表。 let main = runtime .candidate .vm .dict_get(globals, &Value::Text("main".into()))? .ok_or_else(|| anyhow::anyhow!("declare a main function"))?; // 同名全局可能被赋为标量,先解码对象,再由 VM 完整检查可调用契约。 let Value::Object(main) = main else { bail!("main must be callable"); }; // 返回值保留到 has_page/open_value 决策之后;无论打开成功与否都须显式释放。 let value = runtime .candidate .vm .call_with_limits(main, &[], LIMITS) .map_err(|error| runtime_error(&runtime.candidate.vm, error))?; // main 已主动打开 Page 时尊重其选择;否则把返回值经展示协议打开,不能重复创建第二个默认页面。 let opened = if runtime.bridge.has_page()? { Ok(()) } else { // 返回对象按 Presentable/普通值规则进入桥接,不要求所有模块都手写 page.render。 runtime.bridge.open_value(&mut runtime.candidate.vm, &value) }; runtime.candidate.vm.release_value(value)?; opened?; } if !runtime.bridge.has_page()? { // UI 程序必须有实际安排页面,单独计算出一个未打开 Block 不算可发布程序。 bail!("script must call page.render(root) or ui.open(value)"); } runtime.source = source.into(); runtime.ast_nodes = budget.nodes; // 保存完整程序的 Sources 投影,后续 Console 追加时可移入交互记录树;不把 AST 投影当可执行堆对象。 runtime.program_root = Some(architecture(&unit, &plan, namespace, submission)); Ok(runtime) } /// api::worker 新建 Console 域时调用,在未打开页面的空会话中记录原生主窗口尺寸。 pub fn new_with_viewport(id: u64, viewport: Option) -> Result { // 新 Console 不回放旧源码,不复制先前程序的可变对象堆;显示的旧文档仍由 worker 决策保留。 let mut runtime = Self::new(id)?; runtime .bridge .set_viewport(&mut runtime.candidate.vm, viewport)?; Ok(runtime) } /// prepare_program/new_with_viewport 调用,分配持久 Candidate、globals 和 UiBridge,不执行用户代码。 pub fn new(id: u64) -> Result { // Candidate 的 Drop 负责异常返回时的最终清理,不让 bootstrap 半途失败泄漏 VM 根。 let mut candidate = Candidate::new()?; // globals 的拥有根留在 Candidate,实际用户绑定在以后输入中原地更新这个字典。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); // 每个会话安装一份类型域,后续输入继续复用,不按 Console 行重复建立名义类型。 let bridge = ui_bridge::install(&mut candidate.vm, &mut candidate.roots)?; bridge.install_globals(&mut candidate.vm, globals)?; Ok(Self { candidate, bridge, id, inputs: 0, ast_nodes: 0, source: String::new(), nodes: Vec::new(), faulted: false, program_root: None, documents: std::collections::BTreeMap::new(), refresh: None, }) } #[cfg(test)] /// 同步执行一个 Console 输入并排空刷新,返回标量结果及可选新文档。 pub fn execute(&mut self, source: &str, namespace: &str) -> Result { self.execute_mode(source, namespace, None) } /// api::worker 的 Console 输入请求调用,执行真实 globals 上的语句,展示部分允许跨片挂起。 pub fn execute_incremental( &mut self, source: &str, namespace: &str, viewports: &std::collections::BTreeMap, ) -> Result { self.execute_mode(source, namespace, Some(viewports)) } /// execute/execute_incremental 共用入口,先完整编译再执行;解析错误不写业务堆,运行错误不回滚此前成功语句。 fn execute_mode( &mut self, source: &str, namespace: &str, viewports: Option<&std::collections::BTreeMap>, ) -> Result { self.require_idle_refresh()?; if self.faulted { // 故障后保留可见画面不代表 VM 安全可继续变更,必须重置会话或替换完整程序。 bail!( "runtime stopped after a callback or rendering failure; reset the session or replace its program" ); } if source.len() > MAX_SOURCE_BYTES { // 单次 Console 输入也受原始字节预算约束,不能仅靠下面累计 transcript 预算替代。 bail!("input exceeds 1 MiB"); } // 持久 Console 限制累计输入次数、源码文本和稍后的 AST 总节点数,用户需显式重置才能重新获得预算。 if self.inputs >= 256 || self.source.len() + source.len() + 80 > MAX_SOURCE_BYTES { bail!("console session exceeds 256 inputs or 1 MiB; reset the session to continue"); } // 对每次输入保留会话号/输入号作为来源,累计 transcript 的行号稍后再为 Sources 投影偏移。 let source = EndorReader::from_text( format!("console session {} input {}", self.id, self.inputs + 1), source, ); // 输入可以是一段或多段交互语句,语法仍由 Aster 的 parser 决定。 let plan = endor_script::parse(&source)?; // 新输入的结构量先算出,再与会话累计量合并,不为每次输入重新放宽全会话上限。 let mut budget = AstBudget::default(); for ast in plan.roots() { budget.visit_ast_root(ast)?; } if self.ast_nodes + budget.nodes > MAX_AST_NODES { // 此处限制整个持久会话累计结构量;超限发生在记输入号、追加源码和执行用户语句之前。 bail!("console session exceeds 10000 AST nodes; reset the session to continue"); } // new 已建立的字典继续原地使用,保存对象身份和闭包,不能通过历史源码重放重建它。 let globals = self.candidate.globals.expect("live console globals"); // 从实际 globals 枚举已有名称供增量编译,支持前次输入创建的真实对象,而非靠解析历史猜名称。 let names = self .candidate .vm .dict_items(globals)? .iter() .filter_map(|(key, _)| { // 只把字符串键变成静态编译全局名,不向编译器传 VM Value 或托管引用。 if let Value::Text(name) = key { Some(name.clone()) } else { None } }) .collect(); // 所有根先编译完,任何语法/类型编译错误都不会执行输入前半部分。 let compiled = compile_plan_with_globals(&source, &plan, names)?; self.bridge.discard_pending(&mut self.candidate.vm)?; self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; // 解析/编译全部成功后才记本轮输入与源码;之后运行失败仍可能保留此前语句副作用,不做事务回滚。 self.inputs += 1; self.ast_nodes += budget.nodes; if let Some(root) = self.program_root.take() { // 第一次 Console 输入把原完整程序浏览树移入 transcript 节点,不丢原程序的 Sources 内容。 self.nodes.push(root); } if !self.source.is_empty() && !self.source.ends_with('\n') { // 分隔行不改变 VM 代码语义,只保证后续 Sources 行号和注释不粘到上一行末尾。 self.source.push('\n'); } // 交互源码前加 Console input 注释分隔,投影节点行号再整体平移到累计 transcript 中。 let offset = line_starts(&self.source).len(); self.source .push_str(&format!("// Console input {}\n", self.inputs)); self.source.push_str(source.text()); if !self.source.ends_with('\n') { // transcript 的尾部补换行只服务下一条输入的浏览行号,当前执行仍使用独立 SourceUnit 原文。 self.source.push('\n'); } // 本轮输入的代码树先使用局部行号,再用 offset 平移到累计源码中的对应位置。 let mut node = architecture( &source, &plan, &format!("{namespace}:console-{}", self.id), self.inputs as u64, ); node.name = format!("Input {}", self.inputs); shift_node_lines(&mut node, offset); self.nodes.push(node); // 保存执行/刷新失败供下面丢弃未发布请求并给 Sources 标注,已完成赋值不做事务回滚。 let result: Result = (|| { // 多根均分本次硬预算,不让后续片段每个都获得完整一百万条指令。 let limits = ExecutionLimits { max_instructions: LIMITS.max_instructions / compiled.len().max(1), ..LIMITS }; // 依次覆盖为最后一个片段的可显示值;本局部只保存字符串,不延长任意返回对象寿命。 let mut rendered_value = "nil".to_owned(); for (ast, template) in plan.roots().iter().zip(&compiled) { // 代码根只跨一次 execute,先暂存 Result 再 release(code),失败也有清理。 let code = template.load(&mut self.candidate.vm)?; // VM 执行真实 globals,不把本轮 AST 当快照去覆盖旧业务状态。 let executed = self .candidate .vm .execute_with_limits(code, globals, limits) .map_err(|error| runtime_error(&self.candidate.vm, error)); self.candidate.vm.release(code)?; // execute 返回拥有根,后面 repr/全局 _ 更新是否成功都必须释放这份根。 let value = executed?; // 绑定/赋值语句展示其新变量实际值,普通表达式展示返回值;不是仅显示该语句执行的 Unit。 let display = if let AstRoot::Interactive(input) = ast { match &input.statements[0].kind { StmtKind::Binding(binding) => self .candidate .vm .dict_get(globals, &Value::Text(binding.name.text.clone()))? .unwrap_or_else(|| value.clone()), StmtKind::Assignment(assignment) => self .candidate .vm .dict_get(globals, &Value::Text(assignment.name.text.clone()))? .unwrap_or_else(|| value.clone()), _ => value.clone(), } } else { value.clone() }; // 先算 repr 并把实际最后值写入全局 _,保留未命名 UI 对象;随后释放 execute 返回根,错误也逐项传播。 let repr = value_repr(&self.candidate.vm, &display); // _ 的字典强边独立保留最后值,避免匿名 Block 在 Console 显示后马上丢失身份。 let retained = self.candidate .vm .dict_set(globals, Value::Text("_".into()), display); self.candidate.vm.release_value(value)?; retained?; rendered_value = repr?; } // 有业务变更后才推进 UI 稳定循环,可能返回挂起时的 None,worker 继续保留该会话。 let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: rendered_value, document, bindings: self .candidate .vm .dict_items(globals)? .len() .saturating_sub(1), input: self.inputs, }) })(); // 失败丢弃未发布展示请求和原生命令,保留已执行业务写入,并在代码投影上明确标记非事务语义。 if result.is_err() { // 只丢待发布展示与原生命令,不假装撤销此前已完成的用户赋值或资源关闭。 self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; if let Some(node) = self.nodes.last_mut() { // 标记本次已记录的输入节点,保留先前 Sources 记录,不把运行失败误记为从未执行。 node.name .push_str(" (error; earlier statements may remain live)"); } } // 未完成只读续体仍持有临时结果,不在中途执行完整 reclaim;等刷新结束后再回收不可达宿主根。 if !self.has_pending_refresh() { self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } result } /// api::worker 选择空闲等待策略时查询,区分可运行协程与仅等待定时/I/O 的任务,不执行用户代码。 pub fn has_cooperative_tasks(&self) -> bool { self.bridge.has_cooperative_tasks() } /// worker 和本模块写入口查询,刷新未结束时必须继续原续体,不能插入 Console/任务回调写入。 pub fn has_pending_refresh(&self) -> bool { self.refresh.is_some() } /// execute/dispatch/命令提取等入口的共同前置检查,禁止在原只读视觉调用未结束时重入变更。 fn require_idle_refresh(&self) -> Result<()> { anyhow::ensure!( !self.has_pending_refresh(), "finish pending refresh before mutating this runtime" ); Ok(()) } /// prepare_program 后的加载、Console/事件/任务回调后调用,按调用模式同步排空或只推进一个刷新步骤。 fn refresh_after_mutation( &mut self, viewports: Option<&std::collections::BTreeMap>, ) -> Result> { // 无增量视口参数的测试/同步入口排空状态机;worker 提供目标视口时只推进一次并记录重定向依据。 let Some(viewports) = viewports else { return self.refresh_document(); }; // 传入视口表示 worker 增量模式,只前进一步;目标尺寸副本保存供后续重定向比较。 let progress = self.poll_refresh(true)?; if let Some(work) = &mut self.refresh { work.viewports = Some(viewports.clone()); } Ok(match progress { RefreshProgress::Pending => None, RefreshProgress::Complete(document) => document.map(|document| *document), }) } /// 测试同步入口与 drain=true resize 调用,循环推进同一刷新直到稳定,不重执行用户业务语句。 fn refresh_document(&mut self) -> Result> { loop { match self.poll_refresh(false)? { // Pending 仍是活动 Work,只让出 OS 调度机会,不重建 VM 或重新开始源布局。 RefreshProgress::Pending => std::thread::yield_now(), RefreshProgress::Complete(document) => { return Ok(document.map(|document| *document)); } } } } /// refresh_after_mutation/advance_idle_refresh/同步循环共用,消费或保存 RefreshWork 并统一处理故障清理。 fn poll_refresh(&mut self, record_yield: bool) -> Result { // 从 self 取出工作以便可变推进,挂起时放回;首次从已发布各窗口文档克隆初始草稿。 let mut work = self.refresh.take().unwrap_or_else(|| RefreshWork { documents: self.documents.clone(), windows: Default::default(), current: None, root: None, pending: None, paint: None, pass_start: true, pass_changed: false, changed: false, work: 0, viewports: None, }); // 暂存 step 错误以便仍能显式取消 Work 内的续体;不能在这里直接 ? 丢掉 work。 let result = (|| { if self.step_refresh(&mut work)? { return Ok(true); } if record_yield { // 只记录真正把控制权交回 worker 的增量步,同步排空循环不累计这种诊断次数。 self.bridge.note_refresh_yield()?; } Ok(false) })(); match result { Ok(false) => { // 原样保存根/检查点/窗口队列和续体,下次 poll_refresh 从同一拥有域恢复。 self.refresh = Some(work); Ok(RefreshProgress::Pending) } // 收敛后统一组合文档,再恢复主窗口选择;收尾失败也标故障,不把失败草稿当已发布帧。 Ok(true) => { // finish_refresh 验证整份多窗口文档后才更新本会话完成基线,仍由 worker 决定共享发布。 let result = self.finish_refresh(work); self.bridge.select_window(crate::window::MAIN_WINDOW)?; if result.is_err() { self.faulted = true; } result.map(|document| RefreshProgress::Complete(document.map(Box::new))) } Err(error) => { self.faulted = true; // 错误时必须显式取消 paint/layout 的 owning VM 续体并恢复检查点,不能仅丢弃 Rust 工作结构。 // 这里是失败收尾,取消已有 paint 并丢弃配对草稿;只有正常下一量子的 step_refresh 才会恢复 paint。 if let Some(paint) = work.paint.take() { self.bridge.cancel_paint_frame( &mut self.candidate.vm, paint.window, *paint.work, )?; } if let Some(pending) = work.pending.take() { // 若失败发生在布局阶段,取消这份原窗口检查点配套的 Work;不会恢复执行源语句或发布其草稿。 self.bridge .abort_layout_frame(&mut self.candidate.vm, *pending)?; } self.bridge.select_window(crate::window::MAIN_WINDOW)?; Err(error) } } } /// poll_refresh 每量子调用一次,推进稳定循环、一个窗口或一片布局/绘制;true 只表示全轮无新增变化。 fn step_refresh(&mut self, work: &mut RefreshWork) -> Result { // 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。 // 每轮先使属性绑定达到固定点,再取窗口顺序;绑定结果应用与生命周期写入可能令依赖再次失效,因此需要多轮确认。 if work.pass_start { // 生命周期可能改窗口登记,绑定结果可能改视觉属性;新的稳定轮必须重新刷新绑定并获取窗口顺序。 self.bridge.refresh_bindings(&mut self.candidate.vm)?; work.windows = self.bridge.window_ids()?.into(); work.current = None; work.pass_changed = false; work.pass_start = false; return Ok(false); } // 阶段二:逐个选择窗口;一轮发生变化时再扫描一轮,直到依赖不再引起新帧。 let Some(id) = work.current else { // 没有当前窗口时先取队列下一项,本次只做状态推进,下次才进入其 VM 布局/绘制。 if let Some(id) = work.windows.pop_front() { work.current = Some(id); return Ok(false); } // 任何窗口本轮生成新帧,就再扫描一轮检查依赖和挂载回调是否又引起变化,直到整轮无变化。 if work.pass_changed { // 有帧变化就再完整扫一轮,才能发现 hooks/依赖引起的二次变化;不能第一轮结束就发布。 work.pass_start = true; return Ok(false); } return Ok(true); }; self.bridge.select_window(id)?; // 已进入 paint 的窗口直接恢复原绘制续体,不重放业务回调或布局。 if let Some(paint) = work.paint.take() { // 取回配对文档与源 paint Work,不重新 measure/arrange,也不把原业务输入再执行一遍。 let PendingPaint { window, document, work: paint_work, } = *paint; // 同一 owning VM 上恢复固定预算的一片绘制,Complete/Pending 由共同接收器处理。 let progress = self .bridge .resume_paint_frame(&mut self.candidate.vm, *paint_work)?; return Self::accept_window_paint(work, window, document, progress); } // 阶段三:恢复旧布局,或在快照/依赖确实变化时建立新布局。 let progress = if let Some(pending) = work.pending.take() { // 原布局仍挂起时恢复,不能重新提取另一份根让当前续体读写不同展示上下文。 self.bridge .resume_layout_frame(&mut self.candidate.vm, *pending)? } else if let Some(root) = self.bridge.prepare_frame(&mut self.candidate.vm)? { // 双层 Option 内层 None 也代表有空帧工作;保存准确根供布局完成后 paint 复用。 work.root = root.clone(); self.bridge .begin_layout_frame(&mut self.candidate.vm, root)? } else { // 没有任何本窗变化才切下一窗,不能把“需发布空帧”误归为这一无工作分支。 work.current = None; return Ok(false); }; // 未完成布局没有可用完整几何,必须保留 Work;Complete 才可以组装后续待绘制文档。 let frame = match progress { crate::ui_bridge::LayoutProgress::Pending(pending) => { // current 和 root 保留不动,下次量子仍为此窗口续体继续执行。 work.pending = Some(pending); return Ok(false); } crate::ui_bridge::LayoutProgress::Complete(frame) => *frame, }; // 阶段四:几何完成后建立候选文档,再运行 paint;这里尚未向 GUI 发布。 let mut document = RenderDocument { windows: Default::default(), source: self.source.clone(), root: Some(self.program_root.clone().unwrap_or_else(|| CodeNode { id: format!("console-session-{}", self.id), name: format!("Console session {}", self.id), kind: "repl-session".into(), start_line: 1, end_line: self.source.lines().count(), children: self.nodes.clone(), })), frame, ui_root: work.root.take(), }; document.frame.session = self.id; document.validate()?; // 跨所有窗口和稳定轮次累计布局工作,空帧也计一,防止持续无几何变化请求绕过收敛预算。 work.work += document.frame.mounts.len().max(1); if work.work > 200_000 { // 即使每次单帧都有限,失效反馈也可能永不收敛,这个累计预算禁止无限循环刷新。 bail!("layout dependencies did not stabilize within the rendering work budget"); } // Frame 转移给 source_paint,document 暂留其他元数据;PendingPaint 会把两者重新配对保存。 let progress = self.bridge.begin_paint_frame( &mut self.candidate.vm, document.ui_root.clone(), std::mem::take(&mut document.frame), )?; Self::accept_window_paint(work, id, document, progress) } /// step_refresh 启动/恢复 paint 后调用,挂起就保留配对状态,完成就写逐窗草稿并标记需要稳定性复扫。 fn accept_window_paint( work: &mut RefreshWork, id: u64, mut document: RenderDocument, progress: crate::source_paint::Progress, ) -> Result { match progress { crate::source_paint::Progress::Pending(paint) => { // 没完整 DisplayList 时绝不替换 documents,原完整画面仍可供 GUI 使用。 work.paint = Some(Box::new(PendingPaint { window: id, document, work: paint, })); } crate::source_paint::Progress::Complete(draft) => { // 绘制完成的 frame 含实际显示列表,验证通过再进入本轮草稿基线。 document.frame = draft.frame; document.validate()?; // 只有整个绘制 Complete 且验证通过才替换当前窗口草稿;Pending 保留配对文档和 Work,不对 GUI 可见。 work.documents.insert(id, document); work.pass_changed = true; work.changed = true; } } Ok(false) } /// poll_refresh 收到全轮稳定后调用,完成关闭资源清理并生成一次多窗口发布值;不会直接写 NamespaceState。 fn finish_refresh(&mut self, mut work: RefreshWork) -> Result> { // 收敛后完成已关闭窗口退役并删其旧文档;没有实际变化时不制造额外 revision。 self.bridge.finish_closed(&mut self.candidate.vm)?; // 以桥接真实仍登记窗口为准删去旧文档,不让已关闭副窗口在下轮凭缓存“复活”。 let ids = self.bridge.window_ids()?; work.documents.retain(|id, _| ids.contains(id)); if !work.changed { // 无新展示变化仍保存清理后的基线,但返回 None 避免增加无意义外部 revision。 self.documents = work.documents; return Ok(None); } // 主窗可能尚无自己的已画文档,构造带 session 的空基线以容纳新副窗口和源码投影。 let mut main = work .documents .get(&crate::window::MAIN_WINDOW) .cloned() .unwrap_or_else(|| RenderDocument { source: self.source.clone(), frame: crate::document::Frame { session: self.id, ..Default::default() }, ..Default::default() }); main.source = self.source.clone(); // 优先采用源码与当前 transcript 相同的窗口代码投影,避免主窗口未重绘时 Sources 仍展示旧输入树。 main.root = work .documents .values() .find(|document| document.source == self.source) .and_then(|document| document.root.clone()) .or(main.root); // 副窗口统一挂到主文档 windows 一层,子文档自身不再嵌套副窗口,最终 validate 检查完整跨线程值。 main.windows = work .documents .iter() .filter(|(id, _)| **id != crate::window::MAIN_WINDOW) .map(|(id, document)| (*id, document.clone())) .collect(); main.validate()?; self.documents = work.documents; Ok(Some(main)) } #[cfg(test)] /// 同步更新多个窗口尺寸,复用同一运行时并完成必要刷新。 pub fn resize_windows( &mut self, viewports: &std::collections::BTreeMap, ) -> Result> { self.resize_windows_incremental(viewports, true) } /// resize_windows_incremental 检测目标尺寸集合变化时调用,取消当前续体并重建稳定轮,不回滚业务写入。 fn retarget_refresh( &mut self, viewports: &std::collections::BTreeMap, ) -> Result<()> { // 没有挂起刷新就无需重定向,外层随后直接把新尺寸送入正常 set_viewport 路径。 let Some(mut work) = self.refresh.take() else { return Ok(()); }; // 记录实际取消了哪类续体用于诊断;已完成窗口草稿和已发生业务副作用不会因此回滚。 let cancelled = work.pending.is_some(); // 两个独立诊断位记录实际取消阶段,不能把所有重启都记成布局取消。 let cancelled_paint = work.paint.is_some(); if let Some(paint) = work.paint.take() && let Err(error) = self.bridge .cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work) { // 取消 paint 失败后域不再继续正常执行,不把残留续体草稿交给新一轮目标使用。 self.faulted = true; return Err(error); } if let Some(pending) = work.pending.take() && let Err(error) = self .bridge .cancel_layout_frame(&mut self.candidate.vm, *pending) { // layout 取消还要恢复 Mount 检查点,失败同样停域,不能仅删 Pending Rust 值。 self.faulted = true; return Err(error); } if let Err(error) = self.bridge.note_refresh_restart(cancelled, cancelled_paint) { // 统计写入失败也表示桥接状态不可可靠访问,不能保留看似可续跑的刷新并继续执行源程序。 self.faulted = true; return Err(error); } // 重新从全窗口稳定轮开始,清当前选择/旧根并重置此次外部尺寸目标的收敛工作预算;草稿 documents 保留。 work.windows.clear(); work.current = None; work.root = None; work.pass_start = true; work.pass_changed = false; work.work = 0; work.viewports = Some(viewports.clone()); self.refresh = Some(work); Ok(()) } /// api::worker 的窗口 resize 和挂起刷新推进调用;同目标继续原 Work,新目标才显式取消并重定向。 pub fn resize_windows_incremental( &mut self, viewports: &std::collections::BTreeMap, drain: bool, ) -> Result> { if self.faulted { bail!("runtime stopped after a callback or rendering failure"); } // 仅目标视口集合确实不同才取消重定向;同一目标的下一 worker 轮继续原续体,不重复从头布局。 let retarget = self .refresh .as_ref() .and_then(|work| work.viewports.as_ref()) .is_some_and(|previous| previous != viewports); if retarget { // 外部目标确实改变才重启,避免每次 worker 轮询都把慢布局从头做一遍。 self.retarget_refresh(viewports)?; } // 同目标 drain=true 同步排空旧刷新并保存完整结果;drain=false 只推进一量子,不能先执行新的尺寸变更。 let previous = if self.has_pending_refresh() && !retarget { if drain { // 同步调用者要求先完成原目标的完整结果,再处理本次输入的尺寸变更。 let result = self.refresh_document(); self.finish_refresh_update(result)? } else { // 增量轮本次只继续原视觉调用,不能在其只读挂起中插入 set_viewport 写入。 return self.advance_idle_refresh(); } } else { None }; // 过滤仍属于此会话的窗口,GUI 可能还保留旧运行时窗口尺寸记录,不能自动创建同 ID 新资源。 let ids = self.bridge.window_ids()?; // 聚合真正改变尺寸的窗口,重复通知不应引起重新布局或派生版本反馈。 let mut changed = false; // 仅更新桥接仍注册的窗口;旧 GUI 尺寸记录不应在新域里隐式创建窗口资源。 for (id, viewport) in viewports { // viewports 来自宿主尺寸记录,只有桥接当前仍注册的 ID 才有权改输入;其余记录不会分配新窗口。 if ids.contains(id) { // 每次选明确窗口,使 ui.viewport 等隐式上下文读到对应窗口的尺寸输入。 self.bridge.select_window(*id)?; changed |= self .bridge .set_viewport(&mut self.candidate.vm, Some(*viewport))?; } } self.bridge.select_window(crate::window::MAIN_WINDOW)?; // 尺寸没变且无挂起工作则直接返回先前已完成文档,避免制造无意义新帧。 if !changed && !self.has_pending_refresh() { return Ok(previous); } if drain { // 新刷新没变化时仍返回上面已排空的 previous,不让同步路径丢失完整已就绪结果。 let result = self .refresh_document() .map(|document| document.or(previous)); self.finish_refresh_update(result) } else { // 只推进一次,目标集合随 Work 保存,下轮据此辨别“续跑”还是“真正新 resize”。 let result = self.advance_idle_refresh(); if let Some(work) = &mut self.refresh { work.viewports = Some(viewports.clone()); } result } } /// resize 增量路径调用,推进一片并将 Pending 映成 None;仍需 has_pending_refresh 判断是否已空闲。 fn advance_idle_refresh(&mut self) -> Result> { // 先保存错误/完成值供 finish_refresh_update 清理,不能因 poll 报错跳过未发布请求撤销。 let result = self.poll_refresh(true).map(|progress| match progress { RefreshProgress::Pending => None, RefreshProgress::Complete(document) => document.map(|document| *document), }); self.finish_refresh_update(result) } /// resize/advance_idle_refresh 共用收尾,错误时丢未发布命令,空闲后再回收不可达托管根。 fn finish_refresh_update( &mut self, result: Result>, ) -> Result> { if result.is_err() { // GUI 继续使用旧完整文档,失败草稿的副作用命令不能外泄为 Resize/Focus 等原生动作。 self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; } if !self.has_pending_refresh() { // 仍挂起时临时对象属于原视觉调用,不能在其中完整回收;只在续体结束边界 reclaim。 self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } result } /// 同步调用方/测试使用,在 owning VM 推进就绪任务并排空展示;不允许 GUI 或 provider 线程调用 VM。 pub fn poll_tasks(&mut self) -> Result> { self.poll_tasks_mode(None) } /// api::worker 空闲轮调用,每轮至多推进一个任务,然后允许其布局/绘制挂起,给前台输入公平机会。 pub fn poll_tasks_incremental( &mut self, viewports: &std::collections::BTreeMap, ) -> Result> { self.poll_tasks_mode(Some(viewports)) } /// poll_tasks/poll_tasks_incremental 共同执行路径,任务错误停用同一故障域,成功变更走正常刷新状态机。 fn poll_tasks_mode( &mut self, viewports: Option<&std::collections::BTreeMap>, ) -> Result> { // 异步回调不能插入只读布局/绘制续体中,即使后台邮箱已有结果也等刷新空闲边界。 if self.has_pending_refresh() { // 只读视觉续体尚未结束时不取邮箱/回调写业务属性,先让 worker 继续原刷新。 return Ok(None); } if self.faulted { // 故障域不执行任何新用户任务,仅取消已有任务并在确实移除工作后回收引用。 if self.bridge.cancel_tasks(&mut self.candidate.vm)? > 0 { self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } return Ok(None); } // 表示确有任务推进/清理,不等于某个协程已经 Completed;用于决定是否需要 reclaim。 let mut completed = false; // 保留调度/刷新错误供下方统一停域、取消任务、丢弃未发布命令,避免中途直接返回。 let result = (|| { // worker 增量轮至多启动一个任务推进,给前台输入和窗口刷新公平机会;同步路径单轮允许64项。 let limit = if viewports.is_some() { 1 } else { 64 }; if self.bridge.poll_tasks(&mut self.candidate.vm, limit)? == 0 { // 无实际推进就不启动视觉刷新,等待 I/O/计时器不会凭轮询本身制造新帧。 return Ok(None); } completed = true; self.refresh_after_mutation(viewports) })(); if result.is_err() { // 任一共享 VM 任务回调故障会停整个域,不只把当前 Task 标失败后继续执行其他不安全回调。 self.faulted = true; self.bridge.cancel_tasks(&mut self.candidate.vm)?; self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; } // 任务推进或失败后才需回收,并且不能在仍有只读续体时清理其临时根。 if (completed || result.is_err()) && !self.has_pending_refresh() { self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } result } /// api::worker 准备一次发布/错误丢弃时调用,空闲边界才可排空命令,避免从未稳定草稿提前执行 OS 动作。 pub fn take_window_commands(&self) -> Result> { self.require_idle_refresh()?; self.bridge.take_window_commands(&self.candidate.vm) } #[cfg(test)] /// 测试用单窗口尺寸更新入口,遵循与原生尺寸发布相同的校验和刷新规则。 pub fn resize( &mut self, viewport: Option, ) -> Result> { self.require_idle_refresh()?; if self.faulted { bail!("runtime stopped after a callback or rendering failure"); } if !self.bridge.set_viewport(&mut self.candidate.vm, viewport)? { return Ok(None); } // 同步测试 resize 保存刷新错误,随后同样丢弃失败请求并回收引用,不让 ? 跳过清理。 let result = self.refresh_document(); if result.is_err() { self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; } self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; result } /// shutdown 和 ReplSession::drop 调用,显式取消 owning VM 上挂起的 paint/layout,不回放源程序。 fn cancel_pending_refresh(&mut self) -> Result<()> { if let Some(mut work) = self.refresh.take() { // 从会话移走 Work 后逐项清理,检查点/临时 VM 根不能仅靠 Rust 值析构替代。 if let Some(paint) = work.paint.take() { self.bridge.cancel_paint_frame( &mut self.candidate.vm, paint.window, *paint.work, )?; } if let Some(pending) = work.pending.take() { // 与 paint 清理互斥的布局续体还带 Mount 检查点,交桥接层完整取消而非直接丢弃 Rust 句柄。 self.bridge .cancel_layout_frame(&mut self.candidate.vm, *pending)?; } } Ok(()) } /// api::worker 退役运行时调用,取消视觉续体并关闭任务/挂载/窗口;Candidate 的根仍由其 close/Drop 释放。 pub fn shutdown(&mut self) -> Result<(usize, usize, usize)> { // 先取消视觉续体恢复挂载检查点,再判断原域是否允许正常关闭回调,随后标故障禁止后续普通执行。 self.cancel_pending_refresh()?; // 正常域允许卸载钩子,已经故障的域只做宿主清理,不能再次执行不可靠的源回调。 let callbacks = !self.faulted; self.faulted = true; self.bridge.shutdown(&mut self.candidate.vm, callbacks) } /// worker 在故障域处理原生关闭时调用,只退役指定副窗口与缓存,不允许再执行源生命周期钩子。 pub fn retire_window(&mut self, id: u64) -> Result<()> { self.require_idle_refresh()?; // 只用于域已停止后的宿主清理,不能将此路径当作正常 Window.close 源方法的替代。 self.faulted = true; // 专用退役只清宿主资源,不调源生命周期;删除已缓存文档和窗口命令,避免后续重发该副窗口。 self.bridge.retire_window(&mut self.candidate.vm, id)?; self.documents.remove(&id); // 故障退役只排空命令并丢弃,不能把失败域旧 Focus/Resize 发布到当前原生资源。 let _ = self.take_window_commands()?; self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots) } /// api::worker 接受 tab-drop 前调用,验证两端发布版本/页面顺序,普通非拖放信封由 bridge 返回 true。 pub fn accepts_tab_drop(&self, input: &crate::events::Input) -> bool { self.bridge.accepts_tab_drop(&self.candidate.vm, input) } /// api::worker 接纳窗口输入前调用,只查询资源是否仍登记且未 closing,后面仍需 session/Mount 校验。 pub fn accepts_window(&self, id: u64) -> bool { self.bridge.accepts_window(id) } /// 报告该运行时是否已经停止正常业务执行,供 worker 选择故障清理路径。 pub fn is_faulted(&self) -> bool { self.faulted } #[cfg(test)] /// 同步分发一次已校验输入并完成展示刷新。 pub fn dispatch(&mut self, input: &crate::events::Input) -> Result { self.dispatch_mode(input, None) } /// api::worker 的原生/HTTP 输入任务调用,在同一会话执行一次回调,然后分片推进其视觉变化。 pub fn dispatch_incremental( &mut self, input: &crate::events::Input, viewports: &std::collections::BTreeMap, ) -> Result { self.dispatch_mode(input, Some(viewports)) } /// dispatch/dispatch_incremental 共用入口,核对运行时代际/拖放目标,再选窗分派并恢复主窗口上下文。 fn dispatch_mode( &mut self, input: &crate::events::Input, viewports: Option<&std::collections::BTreeMap>, ) -> Result { self.require_idle_refresh()?; if self.faulted { bail!("runtime stopped after a callback or rendering failure"); } // 即使绕过 HTTP 入口直接调用,也再校验会话身份;共享窗口数值 ID 不提供跨 VM 权限。 if input.session != self.id { bail!("event belongs to another runtime session"); } if !self.accepts_tab_drop(input) { // 拖放还需验证目标端最新页面序列,身份不符时不先执行源 moveToWindow 再尝试修复位置。 bail!("drop targets an obsolete presentation"); } // 将 ui.window/viewport 等隐式上下文切到事件所属窗口,分发后统一恢复主窗口。 self.bridge.select_window(input.window)?; // 暂存回调/刷新错误,外层始终尝试恢复主窗口选择、丢弃未发布结果并回收空闲根。 let result = (|| { self.bridge.dispatch(&mut self.candidate.vm, input)?; // 输入已执行一次,后续只续跑视觉刷新,不能为多片 layout 再调用同一 click 回调。 let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: "nil".into(), document, bindings: self .candidate .vm .dict_items(self.candidate.globals.expect("live globals"))? .len() .saturating_sub(1), input: self.inputs, }) })(); self.bridge.select_window(crate::window::MAIN_WINDOW)?; if result.is_err() { self.take_window_commands()?; // 回调失败属于共享 VM 故障,不只关闭单个按钮;丢弃未发布命令与 pending 请求,但不重放已执行写入。 self.faulted = true; self.bridge.discard_pending(&mut self.candidate.vm)?; } if !self.has_pending_refresh() { // 分派后的只读刷新若仍挂起,临时根归原调用持有;只有空闲边界才回收,不干扰下一量子恢复。 self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } result } } impl Drop for ReplSession { /// 析构兜底取消会话中未完成的布局/绘制续体;需要观察清理错误或运行退出钩子的路径仍须显式 shutdown。 fn drop(&mut self) { // Drop 最后兜底取消未完成视觉工作,错误无法从析构返回;需要报告清理错误的路径必须显式 shutdown。 let _ = self.cancel_pending_refresh(); } } /// execute_mode 为本轮 Sources 根调用,递归把局部一基行号平移到累计 Console transcript,不改变执行源码。 fn shift_node_lines(node: &mut CodeNode, offset: usize) { node.start_line += offset; node.end_line += offset; for child in &mut node.children { // 所有后代都保存独立行号,父节点平移不会隐式修正内部函数/表达式的选择范围。 shift_node_lines(child, offset); } } /// execute_mode 在保存全局 _ 前调用,把真实返回值/新绑定值转成显示字符串;返回根仍由调用者释放。 fn value_repr(vm: &Vm, value: &Value) -> Result { Ok(match value { Value::Nil => "nil".into(), Value::Bool(v) => v.to_string(), Value::Text(v) => format!("{v:?}"), Value::Number(v) => v.to_string(), Value::Object(v) => vm.repr(*v)?, }) } /// 拥有 VM、全局字典与宿主保留根;关闭时按顺序释放,失败也必须收尾。 struct Candidate { // 实际解释器/托管堆,仅执行 worker 线程操作,不放入共享 NamespaceState。 vm: Vm, // 宿主安装期间保留的拥有根,close 逆序 pop;ObjectRef Rust 数值本身没有自动 release 行为。 roots: Vec, // 主全局字典也在 roots 中,先清字典断业务根,再统一释放根,避免闭包环残留。 globals: Option, } impl Candidate { /// 创建独立 VM,先不保留任何宿主根或全局字典;后续安装失败也由该候选负责清理。 fn new() -> Result { Ok(Self { vm: Vm::new()?, roots: Vec::new(), globals: None, }) } /// compile/evaluate 显式调用,Candidate::drop 兜底调用;按拥有顺序拆根并验证堆没有遗留对象。 fn close(&mut self) -> Result<()> { // 清全局字典先断业务对象可达根,再逆序释放宿主分配根;最后执行循环回收并验证对象计数归零。 if let Some(globals) = self.globals.take() { // take 使重复 close 不重复清同一身份;先断用户全局再释放类型/函数等宿主根。 self.vm.dict_clear(globals)?; } // pop 逐项移出确保再次 close 不重复释放已成功处理的根;错误仍交显式关闭调用方报告。 while let Some(root) = self.roots.pop() { self.vm.release(root)?; } self.vm.collect_cycles(); if self.vm.object_count() != 0 { // 源程序返回了纯数据也不掩盖资源泄漏,显式 close 会把这个错误报告给调用者。 bail!("candidate VM retained objects after cleanup"); } Ok(()) } } impl Drop for Candidate { /// 析构调用 Candidate.close 释放候选根和 VM,覆盖提前返回/异常展开;析构不能向调用者报告清理错误。 fn drop(&mut self) { // 显式 close 可以报告引用计数/清理错误;Drop 则保证提前返回和异常展开仍经过最终清理边界。 let _ = self.close(); } } /// 本模块和 bridge/tasks 的 VM 错误映射调用,立即取 last_traceback 拼成纯文本,避免后续调用覆盖故障栈。 pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error { // 错误发生后立即读取 VM traceback,保留函数、源文件与指令偏移;无栈信息时直接保留原 VmError。 let locations = vm .last_traceback() .iter() .map(|frame| { // 每帧保留来源文件、函数名和指令位置,不把托管异常对象直接跨线程交付。 format!( "{}:{} in {} (instruction {})", frame.filename, frame.first_line, frame.function, frame.instruction_offset ) }) .collect::>() .join("\n"); if locations.is_empty() { // 无 VM 栈时保留原始 VmError,不能凭当前源码猜一个错误行号。 error.into() } else { // 含栈则附加每帧可读位置,Console/API 不需读取 VM 就能展示错误。 anyhow::anyhow!("{error}\n{locations}") } } #[derive(Default)] /// 编译前的语法节点数量/嵌套预算,限制输入结构复杂度。 struct AstBudget { // 当前递归路径深度,leave_node 归还;不能与累计节点数共用一个计数。 depth: usize, // 已访问节点总量,多个 Script 根共享累计,leave_node 不减少。 nodes: usize, } impl Visitor for AstBudget { type Error = anyhow::Error; /// Aster Visitor 在进入任意语法节点时调用,compile/evaluate/加载/Console 共用这个结构预算。 fn enter_node(&mut self, _span: Span) -> Result<()> { // 每次进入节点累计总数和当前深度;leave 只归还深度,整份输入总量不会因退出子树重置。 self.depth += 1; self.nodes += 1; if self.depth > MAX_AST_DEPTH || self.nodes > MAX_AST_NODES { // 在加载/执行代码前停止,防止巨大源码结构绕过仅约束运行指令的 LIMITS。 bail!("Aster AST exceeds the 64-level or 10000-node host budget"); } Ok(()) } /// 离开当前语法节点时归还嵌套深度;累计节点数保留,不能通过退出子树绕过总量预算。 fn leave_node(&mut self) { self.depth -= 1; } } /// compile/prepare_program/execute_mode 调用,将已解析 AST 投影为 Sources 浏览树,不保存可执行托管引用。 fn architecture( source: &SourceUnit, plan: &SourcePlan, namespace: &str, submission: u64, ) -> CodeNode { // CodeNode ID 由 namespace/submission 和本次递增序号构成,只是 Sources 投影身份,不是托管对象地址。 let mut tree = Architecture { line_starts: line_starts(source.text()), next_id: 0, identity: format!("{namespace}@{submission}"), }; // 把各根按源顺序拼入一个显示根,后续 CodeNode ID 在这一次 tree 构造中递增。 let mut children = Vec::new(); for root in plan.roots() { match root { // 模块展示声明树,脚本展示每段语句树,表达式模式保留单表达式层次。 AstRoot::Module(program) => children.extend( program .declarations .iter() .map(|decl| tree.declaration(decl)), ), AstRoot::Interactive(input) => { children.extend(input.statements.iter().map(|stmt| tree.statement(stmt))) } AstRoot::Expression(expression) => children.push(tree.expression(expression)), } } // 只借根列表取首尾跨度,不克隆整棵 AST;Script 的 span 已由 endor_script 映回原文件。 let roots = plan.roots(); // 整体范围取首根起点至末根终点,Script 的多个交互片段在原始文本中的 span 已恢复偏移。 let span = Span::new( roots.first().map_or(0, |root| root.span().start), roots.last().map_or(0, |root| root.span().end), ); // 代码根种类是 Sources 显示分类,不参与 VM 模块/脚本执行方式的选择。 let kind = if matches!(plan, SourcePlan::Module(_)) { "module" } else { "script" }; tree.node(namespace.to_owned(), kind, span, children) } /// architecture/execute_mode 调用,建立字节偏移→行号的搜索索引,兼容 CRLF、LF 和孤立 CR。 fn line_starts(source: &str) -> Vec { // Span 使用 UTF-8 字节单位,所以不用 chars().enumerate() 的字符索引匹配它。 let bytes = source.as_bytes(); // 按字节扫描行终止符,CRLF 仅在 LF 记一次,孤立 CR 也换行;空文本仍保留第1行起点0。 let mut starts = vec![0]; for (offset, byte) in bytes.iter().enumerate() { if *byte == b'\n' || (*byte == b'\r' && bytes.get(offset + 1) != Some(&b'\n')) { // 记录换行符之后的下一行首,CRLF 只在 LF 分支记录一次,空末行也有可定位起点。 starts.push(offset + 1); } } starts } /// 构造 Sources 语法投影所需的源码与定位辅助状态。 struct Architecture { // 排好序的字节行首,node 通过 partition_point 得到一基行号。 line_starts: Vec, // 单次代码投影中的序号,高水位只用于 UI ID,不是 VM 对象身份。 next_id: usize, // namespace@submission 前缀,区分不同提交/Console 输入产生的 AST 节点。 identity: String, } impl Architecture { /// declaration/function/statement/expression 共用的纯数据分配助手,统一 ID 与半开 span→一基行号转换。 fn node(&mut self, name: String, kind: &str, span: Span, children: Vec) -> CodeNode { // 先取当前序号再递增,父子节点构建顺序可不同但同投影内身份唯一。 let id = self.next_id; self.next_id += 1; // partition_point 返回不大于 offset 的行首数量,天然得到1基行号;span.end 是半开端,先减1找最后实际字符行。 let line = |offset| self.line_starts.partition_point(|start| *start <= offset); CodeNode { id: format!("{}:ast-{id}", self.identity), name, kind: kind.into(), start_line: line(span.start), end_line: line(span.end.saturating_sub(1).max(span.start)), children, } } /// architecture 的模块声明和 statement 的声明分支调用,区分普通函数与含字段/方法的类。 fn declaration(&mut self, declaration: &Decl) -> CodeNode { match &declaration.kind { DeclKind::Function(function) => self.function(function), DeclKind::Class(class) => { // 类投影先列字段再列方法;只复制显示名称和范围,不把声明 AST 当可执行对象保存。 let mut children: Vec<_> = class .fields .iter() .map(|field| self.node(field.name.text.clone(), "field", field.span, vec![])) .collect(); children.extend(class.methods.iter().map(|method| self.function(method))); self.node(class.name.text.clone(), "class", class.span, children) } } } /// declaration 的函数和类方法分支调用,逐条投影函数体;参数包含在源码范围中但不另建显示节点。 fn function(&mut self, function: &frontend::ast::Function) -> CodeNode { // 函数投影按函数体语句顺序递归展开,不单独生成参数节点;展示范围仍覆盖完整函数声明。 let children = function .body .statements .iter() .map(|stmt| self.statement(stmt)) .collect(); self.node( function.name.text.clone(), "function", function.span, children, ) } /// architecture/function/分支体递归调用,生成语句浏览节点,不执行条件、不改变 AST 或业务对象。 fn statement(&mut self, statement: &Stmt) -> CodeNode { match &statement.kind { StmtKind::If(branch) => { // 条件是第一个孩子,随后 then/else 依源顺序展开;树不预测运行时会走哪支。 let mut children = vec![self.expression(&branch.condition)]; children.extend(branch.body.statements.iter().map(|s| self.statement(s))); if let Some(body) = &branch.otherwise { // 没有 else 就不造空显示分支,保持 Sources 仅浏览实际存在的源码。 children.extend(body.statements.iter().map(|s| self.statement(s))); } self.node("if".into(), "if", statement.span, children) } StmtKind::While(loop_) => { // 只展开一次静态循环体,不能按运行次数复制显示节点或重演循环。 let mut children = vec![self.expression(&loop_.condition)]; children.extend(loop_.body.statements.iter().map(|s| self.statement(s))); self.node("while".into(), "while", statement.span, children) } // 成员赋值投影两个子表达式:目标对象和右值,成员名作为本节点显示名称。 StmtKind::MemberAssignment(assignment) => { // 成员访问目标也可能是复合调用,独立投影才能浏览到其中的对象表达式。 let object = self.expression(&assignment.object); // 右值另建子树,不把赋值目标名字混进计算表达式的显示类型。 let value = self.expression(&assignment.value); self.node( assignment.name.text.clone(), "member-assign", statement.span, vec![object, value], ) } StmtKind::Binding(binding) => { // 显示绑定名字,孩子保留初始值表达式及其完整范围。 let child = self.expression(&binding.value); self.node( binding.name.text.clone(), "let", statement.span, vec![child], ) } StmtKind::Assignment(assignment) => { // 和 let 显示类型区分,Sources 可分清声明与之后更新同一名字的语句。 let child = self.expression(&assignment.value); self.node( assignment.name.text.clone(), "assign", statement.span, vec![child], ) } StmtKind::Return(returned) => { // Option::iter 保持无值 return 的孩子列表为空,不凭空补一个 nil AST 节点。 let children = returned .value .iter() .map(|expr| self.expression(expr)) .collect(); self.node("return".into(), "return", statement.span, children) } StmtKind::Expression(expr) => self.expression(expr), StmtKind::Declaration(decl) => self.declaration(decl), } } /// statement 和 expression 自递归调用,保留运算/成员/调用层次,显示标签可截短但源范围不截短。 fn expression(&mut self, expr: &Expr) -> CodeNode { // 三元组统一提供 UI 名称、种类和孩子,最后仍用同一个 node 助手分配 ID/行号。 let (name, kind, children) = match &expr.kind { ExprKind::Name(name) => (name.clone(), "name", vec![]), ExprKind::Number(value) => (value.clone(), "number", vec![]), // 字符串展示名最多40个 Unicode 字符,但 span 保留整段字面量,截断标签不影响源码选择范围。 ExprKind::String(value) => (value.chars().take(40).collect(), "string", vec![]), ExprKind::Symbol(name) => (name.text.clone(), "symbol", vec![]), // 当前代码投影把 Unary 和 Await 共用 unary/negate 显示标签;这是展示简化,不表示 await 执行成取负。 ExprKind::Unary { operand, .. } | ExprKind::Await(operand) => { ("negate".into(), "unary", vec![self.expression(operand)]) } ExprKind::Binary { left, right, .. } => ( "binary".into(), "binary", vec![self.expression(left), self.expression(right)], ), ExprKind::Member { object, name } => { // 成员名作显示标签,对象表达式保留为孩子,类型/属性读取语义不在此求值。 (name.text.clone(), "member", vec![self.expression(object)]) } ExprKind::Call { callee, arguments } => { // 调用目标作为首个孩子,随后按原顺序列实参,Sources 可以区分调用表达式和参数层次。 let mut children = vec![self.expression(callee)]; children.extend(arguments.iter().map(|expr| self.expression(expr))); ("call".into(), "call", children) } }; self.node(name, kind, expr.span, children) } } #[cfg(test)] mod tests; #[cfg(test)] mod manuscript_tests;