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

1525 lines
64 KiB
Rust

//! 把 Aster 编译/执行与 Tessera 展示生命周期连接起来。Candidate 拥有 VM 与宿主根,ReplSession 保留实际运行中的全局环境。
//! 完整源码 PUT 使用隔离候选;Console 输入则在已有 VM 上执行,先前发生的写入不等于可以回放或事务回滚。
//! 刷新状态机遍历窗口并逐步推进布局/绘制。完成所有必要轮次才形成文档;取消会释放挂起调用并恢复未提交的 Mount。
//!
//! Compile complete source in a fresh VM or retain a worker-owned Console VM.
//! Console retains mounted native references and publishes immutable frames after successful inputs.
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,
};
pub const MAX_SOURCE_BYTES: usize = 1_048_576;
const LIMITS: ExecutionLimits = ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
};
const MAX_AST_NODES: usize = 10_000;
const MAX_AST_DEPTH: usize = 64;
/// 把源输入编译并执行为一次独立结果,最终将可展示对象转换为检查后的文档。
pub fn compile(source: &str, namespace: &str, submission: u64) -> Result<RenderDocument> {
if source.len() > MAX_SOURCE_BYTES {
bail!("source exceeds the 1 MiB limit");
}
let source = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source);
let plan = endor_script::parse(&source)?;
// 各 Script 根共享 AST 计数预算,不能通过拆成很多交互片段绕过整份源码节点上限。
let mut budget = AstBudget::default();
for ast in plan.roots() {
budget.visit_ast_root(ast)?;
}
let compiled = compile_plan(&source, &plan)?;
let root = architecture(&source, &plan, namespace, submission);
let mut candidate = Candidate::new()?;
let result: Result<RenderDocument> = (|| {
let globals = candidate.vm.new_dict()?;
candidate.globals = Some(globals);
candidate.roots.push(globals);
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);
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)?;
let ui_root = match &plan {
SourcePlan::Script(_) => bridge
.rendered(&candidate.vm)?
.ok_or_else(|| anyhow::anyhow!("script must call ui.page().render(root)"))?,
// 模块先执行定义再调用 main;若 main 未显式 render,可将其返回值转换成展示根。
SourcePlan::Module(_) => {
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")
})?;
let Value::Object(entry) = entry else {
bail!("main must be a callable function");
};
let value = candidate
.vm
.call_with_limits(entry, &[], LIMITS)
.map_err(|error| runtime_error(&candidate.vm, error))?;
bridge.refresh_bindings(&mut candidate.vm)?;
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?
}
};
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}"))),
}
}
/// Evaluate exactly one expression in a disposable VM; never touch the published page.
/// 在隔离执行域求一个表达式结果;不会把结果作为持久 Console 环境发布。
pub fn evaluate(source: &str) -> Result<String> {
if source.len() > MAX_SOURCE_BYTES {
bail!("expression exceeds the 1 MiB limit");
}
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)?;
let compiled = compiler::compile_ast(&ast, &source, &compiler::CompileOptions::default())
.map_err(|error| source.annotate(error))?;
let mut candidate = Candidate::new()?;
let result = (|| {
let globals = candidate.vm.new_dict()?;
candidate.globals = Some(globals);
candidate.roots.push(globals);
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),
}
}
/// 按 Script/Module 计划编译各根节点,为后续执行准备可加载模板。
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(),
)
}
/// 结合已有全局信息编译本轮输入,保留持久 Console 的名称解析上下文。
fn compile_plan_with_globals(
source: &SourceUnit,
plan: &SourcePlan,
mut globals: BTreeSet<String>,
) -> Result<Vec<compiler::CompiledCode>> {
// Match Aster module behavior for function forward references. Bindings are
// added in source order; every executable statement still uses Aster's own
// single-input Interactive compiler and the candidate's shared globals.
// 先收集全部函数/类声明名以支持前向引用;普通绑定仍按源码执行次序逐段加入已知全局名。
for root in plan.roots() {
if let AstRoot::Interactive(input) = root {
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());
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 {
if let StmtKind::Binding(binding) = &statement.kind {
globals.insert(binding.name.text.clone());
} else if let StmtKind::Assignment(assignment) = &statement.kind {
globals.insert(assignment.name.text.clone());
}
}
}
}
Ok(compiled)
}
/// One real VM, owned only by the existing execution worker. No history replay.
/// The UI bridge shares the same managed object heap and publishes atomic frames.
/// 指定窗口尚未完成的绘制工作,文档与源续体必须保持配对。
struct PendingPaint {
window: u64,
document: RenderDocument,
work: Box<crate::source_paint::Work>,
}
/// 跨 worker 轮次的全窗口刷新状态,保存已完成草稿和当前布局/绘制,直到稳定再发布。
struct RefreshWork {
// 已完成窗口的草稿先留在本轮;所有窗口收敛后才合成一次完整发布。
// RefreshWork 先克隆最近完整文档作为草稿基线,各窗口更新暂存于此;全轮稳定前不写共享 NamespaceState。
documents: std::collections::BTreeMap<u64, RenderDocument>,
windows: std::collections::VecDeque<u64>,
current: Option<u64>,
root: Option<std::sync::Arc<crate::ui_bridge::UiNode>>,
// 续体必须保留其 owning VM;取消时显式关闭,不能靠丢弃 Rust 值。
pending: Option<Box<crate::ui_bridge::PendingLayout>>,
paint: Option<Box<PendingPaint>>,
// pass_changed 要求再跑一轮确认稳定,changed 记录整次刷新是否有可发布变化。
pass_start: bool,
pass_changed: bool,
changed: bool,
work: usize,
// 记录本轮尺寸目标,下一次 worker 轮次据此判断是否需要重定向。
viewports: Option<std::collections::BTreeMap<u64, crate::layout::Size>>,
}
/// 刷新仍在进行或已完成;完成也可能没有新文档需要发布。
enum RefreshProgress {
Pending,
Complete(Option<Box<RenderDocument>>),
}
/// 持久源运行时与其桥接会话,保存实际全局对象、代码投影、全窗口文档和挂起刷新。
pub struct ReplSession {
candidate: Candidate,
bridge: ui_bridge::UiBridge,
pub id: u64,
inputs: usize,
ast_nodes: usize,
source: String,
nodes: Vec<CodeNode>,
// 回调/渲染失败后置位,拒绝继续普通业务输入;保留画面不表示堆还能安全执行。
faulted: bool,
program_root: Option<CodeNode>,
documents: std::collections::BTreeMap<u64, RenderDocument>,
refresh: Option<RefreshWork>,
}
/// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。
pub struct ReplResult {
pub value: String,
pub document: Option<RenderDocument>,
pub bindings: usize,
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)> {
let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?;
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))
}
/// 建立可由 worker 分轮推进的完整源码候选,初始布局不必在一次调用里排空。
pub fn load_incremental(
source: &str,
namespace: &str,
submission: u64,
id: u64,
viewport: Option<crate::layout::Size>,
) -> Result<(Self, Option<RenderDocument>)> {
let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?;
let viewports = viewport
.map(|size| (crate::window::MAIN_WINDOW, size))
.into_iter()
.collect();
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))
}
/// 建立隔离候选、安装宿主根并执行完整源码;候选尚未完成布局时不替换活跃程序。
fn prepare_program(
source: &str,
namespace: &str,
submission: u64,
id: u64,
viewport: Option<crate::layout::Size>,
) -> Result<Self> {
if source.len() > MAX_SOURCE_BYTES {
bail!("source exceeds the 1 MiB limit");
}
let unit = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source);
let plan = endor_script::parse(&unit)?;
let mut budget = AstBudget::default();
for ast in plan.roots() {
budget.visit_ast_root(ast)?;
}
let compiled = compile_plan(&unit, &plan)?;
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");
let limits = if matches!(plan, SourcePlan::Script(_)) {
ExecutionLimits {
max_instructions: LIMITS.max_instructions / compiled.len().max(1),
..LIMITS
}
} else {
LIMITS
};
for template in &compiled {
let code = template.load(&mut runtime.candidate.vm)?;
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(_)) {
let main = runtime
.candidate
.vm
.dict_get(globals, &Value::Text("main".into()))?
.ok_or_else(|| anyhow::anyhow!("declare a main function"))?;
let Value::Object(main) = main else {
bail!("main must be callable");
};
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 {
runtime.bridge.open_value(&mut runtime.candidate.vm, &value)
};
runtime.candidate.vm.release_value(value)?;
opened?;
}
if !runtime.bridge.has_page()? {
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)
}
/// 建立空的持久 Console 运行时,并安装当前逻辑视口输入。
pub fn new_with_viewport(id: u64, viewport: Option<crate::layout::Size>) -> Result<Self> {
let mut runtime = Self::new(id)?;
runtime
.bridge
.set_viewport(&mut runtime.candidate.vm, viewport)?;
Ok(runtime)
}
/// 建立真实持久 VM、全局字典与 UI 桥接根,尚未执行用户输入或产生窗口帧。
pub fn new(id: u64) -> Result<Self> {
let mut candidate = Candidate::new()?;
let globals = candidate.vm.new_dict()?;
candidate.globals = Some(globals);
candidate.roots.push(globals);
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)
}
/// 执行一次输入的业务部分,再让展示刷新以可恢复工作跨 worker 轮次推进。
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))
}
/// 在当前真实 VM 中编译并执行新输入,再按同步或分片模式刷新展示;已发生的业务写入不会被源重放回滚。
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 {
bail!(
"runtime stopped after a callback or rendering failure; reset the session or replace its program"
);
}
if source.len() > MAX_SOURCE_BYTES {
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");
}
let source = EndorReader::from_text(
format!("console session {} input {}", self.id, self.inputs + 1),
source,
);
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");
}
let globals = self.candidate.globals.expect("live console globals");
// 从实际 globals 枚举已有名称供增量编译,支持前次输入创建的真实对象,而非靠解析历史猜名称。
let names = self
.candidate
.vm
.dict_items(globals)?
.iter()
.filter_map(|(key, _)| {
if let Value::Text(name) = key {
Some(name.clone())
} else {
None
}
})
.collect();
let compiled = compile_plan_with_globals(&source, &plan, names)?;
// Parsing/compilation finish before any new statement runs. Successful earlier
// statements in a runtime-failed input remain live, as in an ordinary REPL.
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() {
self.nodes.push(root);
}
if !self.source.is_empty() && !self.source.ends_with('\n') {
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') {
self.source.push('\n');
}
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);
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) {
let code = template.load(&mut self.candidate.vm)?;
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)?;
let value = executed?;
// Keep the actual last result accessible as `_`, including unnamed UI objects.
// 绑定/赋值语句展示其新变量实际值,普通表达式展示返回值;不是仅显示该语句执行的 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);
let retained =
self.candidate
.vm
.dict_set(globals, Value::Text("_".into()), display);
self.candidate.vm.release_value(value)?;
retained?;
rendered_value = repr?;
}
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() {
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
}
/// 查询调度器是否仍有需要主动推进的源协程工作。
pub fn has_cooperative_tasks(&self) -> bool {
self.bridge.has_cooperative_tasks()
}
/// 检查是否存在尚未结束的视觉刷新工作,决定能否接收新的业务变更。
pub fn has_pending_refresh(&self) -> bool {
self.refresh.is_some()
}
/// 拒绝在未完成的只读刷新内部再次进行业务变更,防止同一 VM 重入。
fn require_idle_refresh(&self) -> Result<()> {
anyhow::ensure!(
!self.has_pending_refresh(),
"finish pending refresh before mutating this runtime"
);
Ok(())
}
/// 在业务变化后开始或继续展示刷新,保证后续输入不会进入未结束的只读视觉调用。
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();
};
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),
})
}
/// 同步排空当前刷新状态机,直到全窗口收敛或出现错误。
fn refresh_document(&mut self) -> Result<Option<RenderDocument>> {
loop {
match self.poll_refresh(false)? {
RefreshProgress::Pending => std::thread::yield_now(),
RefreshProgress::Complete(document) => {
return Ok(document.map(|document| *document));
}
}
}
}
/// 推进一次刷新状态机,保存挂起的 Work;只有全窗口收敛时才生成可发布文档。
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,
});
let result = (|| {
if self.step_refresh(&mut work)? {
return Ok(true);
}
if record_yield {
self.bridge.note_refresh_yield()?;
}
Ok(false)
})();
match result {
Ok(false) => {
self.refresh = Some(work);
Ok(RefreshProgress::Pending)
}
// 收敛后统一组合文档,再恢复主窗口选择;收尾失败也标故障,不把失败草稿当已发布帧。
Ok(true) => {
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 后只恢复其原续体和配对文档,不重复布局,也不重跑输入业务语句。
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() {
self.bridge
.abort_layout_frame(&mut self.candidate.vm, *pending)?;
}
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
Err(error)
}
}
}
/// 执行一个刷新步骤:刷新绑定、选择窗口、准备快照、推进布局或绘制,并决定是否需要下一轮。
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 {
if let Some(id) = work.windows.pop_front() {
work.current = Some(id);
return Ok(false);
}
// 任何窗口本轮生成新帧,就再扫描一轮检查依赖和挂载回调是否又引起变化,直到整轮无变化。
if work.pass_changed {
work.pass_start = true;
return Ok(false);
}
return Ok(true);
};
self.bridge.select_window(id)?;
// 已进入 paint 的窗口直接恢复原绘制续体,不重放业务回调或布局。
if let Some(paint) = work.paint.take() {
let PendingPaint {
window,
document,
work: paint_work,
} = *paint;
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)? {
work.root = root.clone();
self.bridge
.begin_layout_frame(&mut self.candidate.vm, root)?
} else {
work.current = None;
return Ok(false);
};
let frame = match progress {
crate::ui_bridge::LayoutProgress::Pending(pending) => {
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");
}
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)
}
/// 接收指定窗口的绘制进度,将挂起 Work 保留或把完整文档存入本轮结果。
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) => {
work.paint = Some(Box::new(PendingPaint {
window: id,
document,
work: paint,
}));
}
crate::source_paint::Progress::Complete(draft) => {
document.frame = draft.frame;
document.validate()?;
// 只有整个绘制 Complete 且验证通过才替换当前窗口草稿;Pending 保留配对文档和 Work,不对 GUI 可见。
work.documents.insert(id, document);
work.pass_changed = true;
work.changed = true;
}
}
Ok(false)
}
/// 收敛后组合所有窗口文档,保留本轮完整结果;没有变化时不额外制造发布。
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 {
self.documents = work.documents;
return Ok(None);
}
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)
}
/// 用最新视口替换旧目标,显式取消未完成布局,同时保留已完成窗口结果和已发生的业务效果。
fn retarget_refresh(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> Result<()> {
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)
{
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)
{
self.faulted = true;
return Err(error);
}
if let Err(error) = self.bridge.note_refresh_restart(cancelled, cancelled_paint) {
self.faulted = true;
return Err(error);
}
// Keep completed window drafts and callback effects. Only the pending
// window's speculative Mount state was restored. A new external target
// begins a fresh convergence budget, not a continuation of a bad cycle.
// 重新从全窗口稳定轮开始,清当前选择/旧根并重置此次外部尺寸目标的收敛工作预算;草稿 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(())
}
/// 把各窗口最新尺寸送入同一运行时;视口更新可跨轮次挂起,也可由调用方同步排空。
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 {
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 {
return self.advance_idle_refresh();
}
} else {
None
};
let ids = self.bridge.window_ids()?;
let mut changed = false;
// 仅更新桥接仍注册的窗口;旧 GUI 尺寸记录不应在新域里隐式创建窗口资源。
for (id, viewport) in viewports {
if ids.contains(id) {
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 {
let result = self
.refresh_document()
.map(|document| document.or(previous));
self.finish_refresh_update(result)
} else {
let result = self.advance_idle_refresh();
if let Some(work) = &mut self.refresh {
work.viewports = Some(viewports.clone());
}
result
}
}
/// 在空闲轮次推进一个刷新步骤,完成时执行统一结果收尾。
fn advance_idle_refresh(&mut self) -> Result<Option<RenderDocument>> {
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)
}
/// 统一处理刷新失败、未发布状态清理和运行时引用回收。
fn finish_refresh_update(
&mut self,
result: Result<Option<RenderDocument>>,
) -> Result<Option<RenderDocument>> {
if result.is_err() {
self.take_window_commands()?;
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
}
/// Deliver ready work on the owning VM worker, never from the GUI or a
/// provider thread. A failed callback stops the same shared fault domain.
/// 同步推进可交付任务和必要刷新,供不使用分片 worker 的调用方使用。
pub fn poll_tasks(&mut self) -> Result<Option<RenderDocument>> {
self.poll_tasks_mode(None)
}
/// Idle delivery starts at most one callback, then parks its layout work.
/// 按有界任务交付量推进,再以可挂起刷新处理变化。
pub fn poll_tasks_incremental(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
) -> Result<Option<RenderDocument>> {
self.poll_tasks_mode(Some(viewports))
}
/// 按本轮预算交付已完成任务或推进协程,再对其业务变化执行相同的展示刷新流程。
fn poll_tasks_mode(
&mut self,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
) -> Result<Option<RenderDocument>> {
// 异步回调不能插入只读布局/绘制续体中,即使后台邮箱已有结果也等刷新空闲边界。
if self.has_pending_refresh() {
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);
}
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 {
return Ok(None);
}
completed = true;
self.refresh_after_mutation(viewports)
})();
if result.is_err() {
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
}
/// 取得本轮已稳定运行时的窗口命令,挂起刷新时禁止提前取走。
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);
}
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
}
/// 关闭尚未完成的刷新任务,释放续体并恢复未提交 Mount;不能只丢弃 Work 结构。
fn cancel_pending_refresh(&mut self) -> Result<()> {
if let Some(mut work) = self.refresh.take() {
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() {
self.bridge
.cancel_layout_frame(&mut self.candidate.vm, *pending)?;
}
}
Ok(())
}
/// 有界关闭任务、挂载和窗口,释放运行时根,并报告清理结果。
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)
}
/// 撤销指定窗口的运行时展示资源,防止后续刷新重新发布它。
pub fn retire_window(&mut self, id: u64) -> Result<()> {
self.require_idle_refresh()?;
// This path is exclusively host cleanup after the domain has stopped.
// No language functions, layout, paint or lifecycle hooks may run here.
self.faulted = true;
// 专用退役只清宿主资源,不调源生命周期;删除已缓存文档和窗口命令,避免后续重发该副窗口。
self.bridge.retire_window(&mut self.candidate.vm, id)?;
self.documents.remove(&id);
let _ = self.take_window_commands()?;
self.bridge
.reclaim(&mut self.candidate.vm, &mut self.candidate.roots)
}
/// 查询当前运行时是否仍接受对应的窗口或拖放目标。
pub fn accepts_tab_drop(&self, input: &crate::events::Input) -> bool {
self.bridge.accepts_tab_drop(&self.candidate.vm, input)
}
/// 查询当前运行时是否仍接受对应的窗口或拖放目标。
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)
}
/// 分发一次输入回调,再把视觉刷新交给 worker 分片推进。
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))
}
/// 在正确窗口上下文执行输入回调,再刷新展示;故障时停止受影响域并清理未完成工作。
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) {
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)?;
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();
}
}
/// 递归修正代码投影的源行号,使拼接输入仍可定位到原始文本。
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);
}
}
/// 把 VM 返回值转换为 Console 的可显示表示,不让借用的托管引用跨线程。
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 {
vm: Vm,
roots: Vec<ObjectRef>,
globals: Option<ObjectRef>,
}
impl Candidate {
/// 创建独立 VM,先不保留任何宿主根或全局字典;后续安装失败也由该候选负责清理。
fn new() -> Result<Self> {
Ok(Self {
vm: Vm::new()?,
roots: Vec::new(),
globals: None,
})
}
/// 释放候选拥有的宿主根并清理 VM;通过显式关闭保证错误也能被报告。
fn close(&mut self) -> Result<()> {
// 清全局字典先断业务对象可达根,再逆序释放宿主分配根;最后执行循环回收并验证对象计数归零。
if let Some(globals) = self.globals.take() {
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 {
bail!("candidate VM retained objects after cleanup");
}
Ok(())
}
}
impl Drop for Candidate {
/// 析构调用 Candidate.close 释放候选根和 VM,覆盖提前返回/异常展开;析构不能向调用者报告清理错误。
fn drop(&mut self) {
// 显式 close 可以报告引用计数/清理错误;Drop 则保证提前返回和异常展开仍经过最终清理边界。
let _ = self.close();
}
}
/// 把 VM 错误与当前源位置/调用栈组合成用户可读的诊断。
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() {
error.into()
} else {
anyhow::anyhow!("{error}\n{locations}")
}
}
#[derive(Default)]
/// 编译前的语法节点数量/嵌套预算,限制输入结构复杂度。
struct AstBudget {
depth: usize,
nodes: usize,
}
impl Visitor for AstBudget {
type Error = anyhow::Error;
/// 更新源结构遍历的深度/数量预算,拒绝超出允许范围的语法树。
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 {
bail!("Aster AST exceeds the 64-level or 10000-node host budget");
}
Ok(())
}
/// 离开当前语法节点时归还嵌套深度;累计节点数保留,不能通过退出子树绕过总量预算。
fn leave_node(&mut self) {
self.depth -= 1;
}
}
/// 把输入语法结构转换为 Sources 工具使用的 CodeNode 层次,保存原始源码位置。
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}"),
};
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)),
}
}
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),
);
let kind = if matches!(plan, SourcePlan::Module(_)) {
"module"
} else {
"script"
};
tree.node(namespace.to_owned(), kind, span, children)
}
/// 建立源文本每行起点表,供字节 span 转换为行列位置。
fn line_starts(source: &str) -> Vec<usize> {
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')) {
starts.push(offset + 1);
}
}
starts
}
/// 构造 Sources 语法投影所需的源码与定位辅助状态。
struct Architecture {
line_starts: Vec<usize>,
next_id: usize,
identity: String,
}
impl Architecture {
/// 分配本投影内节点 ID,并将半开字节 span 映射为1基起止行,组装 CodeNode。
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,
}
}
/// 区分函数与类声明;类下先投影字段,再投影方法。
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)
}
}
}
/// 将函数声明及其体内语句投影为一个带源码范围的 function 节点。
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,
)
}
/// 按赋值、返回、表达式和声明种类构建 Sources 层次,保留真实子表达式关系。
fn statement(&mut self, statement: &Stmt) -> CodeNode {
match &statement.kind {
// 成员赋值投影两个子表达式:目标对象和右值,成员名作为本节点显示名称。
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 child = self.expression(&assignment.value);
self.node(
assignment.name.text.clone(),
"assign",
statement.span,
vec![child],
)
}
StmtKind::Return(returned) => {
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),
}
}
/// 递归投影表达式层次,用简短文本作显示名,并保留完整原始 span。
fn expression(&mut self, expr: &Expr) -> CodeNode {
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;