1383 lines
48 KiB
Rust
1383 lines
48 KiB
Rust
//! 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)?;
|
|
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)?;
|
|
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)"))?,
|
|
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),
|
|
});
|
|
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)
|
|
})();
|
|
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.
|
|
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);
|
|
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))?;
|
|
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),
|
|
}
|
|
}
|
|
|
|
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(),
|
|
)
|
|
}
|
|
|
|
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))?,
|
|
);
|
|
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>,
|
|
}
|
|
|
|
struct RefreshWork {
|
|
documents: std::collections::BTreeMap<u64, RenderDocument>,
|
|
windows: std::collections::VecDeque<u64>,
|
|
current: Option<u64>,
|
|
root: Option<std::sync::Arc<crate::ui_bridge::UiNode>>,
|
|
pending: Option<Box<crate::ui_bridge::PendingLayout>>,
|
|
paint: Option<Box<PendingPaint>>,
|
|
pass_start: bool,
|
|
pass_changed: bool,
|
|
changed: bool,
|
|
work: usize,
|
|
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>,
|
|
}
|
|
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))
|
|
}
|
|
|
|
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))?;
|
|
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)?;
|
|
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))?;
|
|
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;
|
|
runtime.program_root = Some(architecture(&unit, &plan, namespace, submission));
|
|
Ok(runtime)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)]
|
|
pub fn execute(&mut self, source: &str, namespace: &str) -> Result<ReplResult> {
|
|
self.execute_mode(source, namespace, None)
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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");
|
|
}
|
|
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");
|
|
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');
|
|
}
|
|
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.
|
|
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()
|
|
};
|
|
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)");
|
|
}
|
|
}
|
|
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()
|
|
}
|
|
|
|
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>> {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn poll_refresh(&mut self, record_yield: bool) -> Result<RefreshProgress> {
|
|
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;
|
|
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)?;
|
|
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,
|
|
};
|
|
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)
|
|
}
|
|
|
|
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()?;
|
|
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>> {
|
|
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();
|
|
main.root = work
|
|
.documents
|
|
.values()
|
|
.find(|document| document.source == self.source)
|
|
.and_then(|document| document.root.clone())
|
|
.or(main.root);
|
|
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.
|
|
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");
|
|
}
|
|
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)?;
|
|
}
|
|
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;
|
|
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.
|
|
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 = (|| {
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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");
|
|
}
|
|
if input.session != self.id {
|
|
bail!("event belongs to another runtime session");
|
|
}
|
|
if !self.accepts_tab_drop(input) {
|
|
bail!("drop targets an obsolete presentation");
|
|
}
|
|
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()?;
|
|
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 {
|
|
fn drop(&mut self) {
|
|
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);
|
|
}
|
|
}
|
|
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)?,
|
|
})
|
|
}
|
|
|
|
struct Candidate {
|
|
vm: Vm,
|
|
roots: Vec<ObjectRef>,
|
|
globals: Option<ObjectRef>,
|
|
}
|
|
|
|
impl Candidate {
|
|
fn new() -> Result<Self> {
|
|
Ok(Self {
|
|
vm: Vm::new()?,
|
|
roots: Vec::new(),
|
|
globals: None,
|
|
})
|
|
}
|
|
|
|
fn close(&mut self) -> Result<()> {
|
|
if let Some(globals) = self.globals.take() {
|
|
self.vm.dict_clear(globals)?;
|
|
}
|
|
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 {
|
|
fn drop(&mut self) {
|
|
// Explicit close reports accounting errors; dropping the fresh VM is
|
|
// also the final cleanup boundary on every early return or unwind.
|
|
let _ = self.close();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error {
|
|
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<()> {
|
|
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;
|
|
}
|
|
}
|
|
|
|
fn architecture(
|
|
source: &SourceUnit,
|
|
plan: &SourcePlan,
|
|
namespace: &str,
|
|
submission: u64,
|
|
) -> CodeNode {
|
|
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();
|
|
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)
|
|
}
|
|
|
|
fn line_starts(source: &str) -> Vec<usize> {
|
|
let bytes = source.as_bytes();
|
|
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
|
|
}
|
|
|
|
struct Architecture {
|
|
line_starts: Vec<usize>,
|
|
next_id: usize,
|
|
identity: String,
|
|
}
|
|
|
|
impl Architecture {
|
|
fn node(&mut self, name: String, kind: &str, span: Span, children: Vec<CodeNode>) -> CodeNode {
|
|
let id = self.next_id;
|
|
self.next_id += 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) => {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
|
|
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![]),
|
|
ExprKind::String(value) => (value.chars().take(40).collect(), "string", vec![]),
|
|
ExprKind::Symbol(name) => (name.text.clone(), "symbol", vec![]),
|
|
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 } => {
|
|
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;
|