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

1754 lines
89 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.
//! 把 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<RenderDocument> {
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<RenderDocument> = (|| {
// 同一程序的所有段共享一个 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<String> {
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<Vec<compiler::CompiledCode>> {
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<String>,
) -> Result<Vec<compiler::CompiledCode>> {
// 先收集全部函数/类声明名以支持前向引用;普通绑定仍按源码执行次序逐段加入已知全局名。
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<String>) {
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<crate::source_paint::Work>,
}
/// 跨 worker 轮次的全窗口刷新状态,保存已完成草稿和当前布局/绘制,直到稳定再发布。
struct RefreshWork {
// 已完成窗口的草稿先留在本轮;所有窗口收敛后才合成一次完整发布。
// RefreshWork 先克隆最近完整文档作为草稿基线,各窗口更新暂存于此;全轮稳定前不写共享 NamespaceState。
documents: std::collections::BTreeMap<u64, RenderDocument>,
// 当前稳定性遍历轮剩余窗口,按 bridge 的退旧优先次序排队。
windows: std::collections::VecDeque<u64>,
// 本片正在推进窗口,pending/paint 的上下文必须与其一致。
current: Option<u64>,
// 当前帧不可变根草稿,布局完成再绘制仍使用这一轮同一份快照。
root: Option<std::sync::Arc<crate::ui_bridge::UiNode>>,
// 续体必须保留其 owning VM;取消时显式关闭,不能靠丢弃 Rust 值。
pending: Option<Box<crate::ui_bridge::PendingLayout>>,
// 与 pending 布局阶段互斥的未完成绘制,完成后才把当前窗口文档写进 documents。
paint: Option<Box<PendingPaint>>,
// pass_changed 要求再跑一轮确认稳定,changed 记录整次刷新是否有可发布变化。
pass_start: bool,
// 一轮是否有变化,决定下一轮还需确认稳定;不等于最终对外是否已有变化。
pass_changed: bool,
// 整次刷新累计变化,用于稳定后决定返回文档还是 None。
changed: bool,
// 整次刷新工作量预算,跨多个 worker 量子累计,不能让每片各得一份完整收敛额度。
work: usize,
// 记录本轮尺寸目标,下一次 worker 轮次据此判断是否需要重定向。
viewports: Option<std::collections::BTreeMap<u64, crate::layout::Size>>,
}
/// 刷新仍在进行或已完成;完成也可能没有新文档需要发布。
enum RefreshProgress {
// worker 保留整个 ReplSession,下次继续已有布局/绘制续体,不接新业务写入。
Pending,
// 已稳定;None 为没有新文档,Some 才有可以让 worker 校验/发布的完整值。
Complete(Option<Box<RenderDocument>>),
}
/// 持久源运行时与其桥接会话,保存实际全局对象、代码投影、全窗口文档和挂起刷新。
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<CodeNode>,
// 回调/渲染失败后置位,拒绝继续普通业务输入;保留画面不表示堆还能安全执行。
faulted: bool,
// 完整程序的 Sources 根,首次 Console 追加时移入 nodes 保留原程序浏览记录。
program_root: Option<CodeNode>,
// 最近完成的逐窗基线文档;未稳定刷新只改 RefreshWork 草稿,不提前替换共享发布状态。
documents: std::collections::BTreeMap<u64, RenderDocument>,
// 同一个 VM 上至多一份跨片刷新,所有业务写入口先 require_idle_refresh。
refresh: Option<RefreshWork>,
}
/// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。
pub struct ReplResult {
// 用于 Console/API 的字符串结果,不跨线程暴露拥有根 Value。
pub value: String,
// 只有本轮已经稳定完成刷新才返回完整文档,挂起或无变化时为 None。
pub document: Option<RenderDocument>,
// 当前 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<crate::layout::Size>,
) -> 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<crate::layout::Size>,
) -> Result<(Self, Option<RenderDocument>)> {
// 源程序执行在隔离 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<crate::layout::Size>,
) -> Result<Self> {
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<crate::layout::Size>) -> Result<Self> {
// 新 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<Self> {
// 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<ReplResult> {
self.execute_mode(source, namespace, None)
}
/// api::worker 的 Console 输入请求调用,执行真实 globals 上的语句,展示部分允许跨片挂起。
pub fn execute_incremental(
&mut self,
source: &str,
namespace: &str,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> Result<ReplResult> {
self.execute_mode(source, namespace, Some(viewports))
}
/// execute/execute_incremental 共用入口,先完整编译再执行;解析错误不写业务堆,运行错误不回滚此前成功语句。
fn execute_mode(
&mut self,
source: &str,
namespace: &str,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
) -> Result<ReplResult> {
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<ReplResult> = (|| {
// 多根均分本次硬预算,不让后续片段每个都获得完整一百万条指令。
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<u64, crate::layout::Size>>,
) -> Result<Option<RenderDocument>> {
// 无增量视口参数的测试/同步入口排空状态机;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<Option<RenderDocument>> {
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<RefreshProgress> {
// 从 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<bool> {
// 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。
// 每轮先使属性绑定达到固定点,再取窗口顺序;绑定结果应用与生命周期写入可能令依赖再次失效,因此需要多轮确认。
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<bool> {
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<Option<RenderDocument>> {
// 收敛后完成已关闭窗口退役并删其旧文档;没有实际变化时不制造额外 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<u64, crate::layout::Size>,
) -> Result<Option<RenderDocument>> {
self.resize_windows_incremental(viewports, true)
}
/// resize_windows_incremental 检测目标尺寸集合变化时调用,取消当前续体并重建稳定轮,不回滚业务写入。
fn retarget_refresh(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> 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<u64, crate::layout::Size>,
drain: bool,
) -> Result<Option<RenderDocument>> {
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<Option<RenderDocument>> {
// 先保存错误/完成值供 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<Option<RenderDocument>>,
) -> Result<Option<RenderDocument>> {
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<Option<RenderDocument>> {
self.poll_tasks_mode(None)
}
/// api::worker 空闲轮调用,每轮至多推进一个任务,然后允许其布局/绘制挂起,给前台输入公平机会。
pub fn poll_tasks_incremental(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> Result<Option<RenderDocument>> {
self.poll_tasks_mode(Some(viewports))
}
/// poll_tasks/poll_tasks_incremental 共同执行路径,任务错误停用同一故障域,成功变更走正常刷新状态机。
fn poll_tasks_mode(
&mut self,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
) -> Result<Option<RenderDocument>> {
// 异步回调不能插入只读布局/绘制续体中,即使后台邮箱已有结果也等刷新空闲边界。
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<Vec<crate::window::Command>> {
self.require_idle_refresh()?;
self.bridge.take_window_commands(&self.candidate.vm)
}
#[cfg(test)]
/// 测试用单窗口尺寸更新入口,遵循与原生尺寸发布相同的校验和刷新规则。
pub fn resize(
&mut self,
viewport: Option<crate::layout::Size>,
) -> Result<Option<RenderDocument>> {
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<ReplResult> {
self.dispatch_mode(input, None)
}
/// api::worker 的原生/HTTP 输入任务调用,在同一会话执行一次回调,然后分片推进其视觉变化。
pub fn dispatch_incremental(
&mut self,
input: &crate::events::Input,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> Result<ReplResult> {
self.dispatch_mode(input, Some(viewports))
}
/// dispatch/dispatch_incremental 共用入口,核对运行时代际/拖放目标,再选窗分派并恢复主窗口上下文。
fn dispatch_mode(
&mut self,
input: &crate::events::Input,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
) -> Result<ReplResult> {
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<String> {
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<ObjectRef>,
// 主全局字典也在 roots 中,先清字典断业务根,再统一释放根,避免闭包环残留。
globals: Option<ObjectRef>,
}
impl Candidate {
/// 创建独立 VM,先不保留任何宿主根或全局字典;后续安装失败也由该候选负责清理。
fn new() -> Result<Self> {
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::<Vec<_>>()
.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<usize> {
// 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<usize>,
// 单次代码投影中的序号,高水位只用于 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>) -> 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;