Remove is_tail field from Call node

The `is_tail` field on the `BoundKind::Call` node was an intermediate
representation for tail-call optimization and is no longer needed as a
distinct field. The TCO pass now embeds this information directly into
the `RuntimeMetadata` of the `ExecNode`, which is the VM's internal AST.
This simplifies the `BoundKind::Call` structure and removes redundant
information.
This commit is contained in:
Michael Schimmel
2026-02-21 22:01:03 +01:00
parent 78dff07930
commit ff1024ee49
10 changed files with 236 additions and 561 deletions
+1 -2
View File
@@ -233,8 +233,7 @@ impl Binder {
Ok(self.make_node(node.identity.clone(), BoundKind::Call {
callee: Box::new(callee),
args: Box::new(args),
is_tail: false
args: Box::new(args)
}))
},
+3 -4
View File
@@ -83,7 +83,6 @@ pub enum BoundKind<T = ()> {
Call {
callee: Box<BoundNode<T>>,
args: Box<BoundNode<T>>,
is_tail: bool,
},
Block {
@@ -141,8 +140,8 @@ where T: PartialEq {
BoundKind::Lambda { params: pb, upvalues: ub, body: bb, positional_count: pcb }) => {
pa == pb && ua == ub && ba == bb && pca == pcb
}
(BoundKind::Call { callee: ca, args: aa, is_tail: ta }, BoundKind::Call { callee: cb, args: ab, is_tail: tb }) => {
ca == cb && aa == ab && ta == tb
(BoundKind::Call { callee: ca, args: aa }, BoundKind::Call { callee: cb, args: ab }) => {
ca == cb && aa == ab
}
(BoundKind::Block { exprs: ea }, BoundKind::Block { exprs: eb }) => {
ea == eb
@@ -184,7 +183,7 @@ impl<T> BoundKind<T> {
};
format!("LAMBDA({}, Captures:{})", p_str, upvalues.len())
},
BoundKind::Call { is_tail, .. } => if *is_tail { "T-CALL".to_string() } else { "CALL".to_string() },
BoundKind::Call { .. } => "CALL".to_string(),
BoundKind::Block { .. } => "BLOCK".to_string(),
BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
BoundKind::Record { fields } => format!("RECORD({})", fields.len()),
+2 -3
View File
@@ -137,9 +137,8 @@ impl Dumper {
self.log(&format!("Parameter (Name: '{}', Slot: {})", name.name, slot), node);
}
BoundKind::Call { callee, args, is_tail } => {
let label = if *is_tail { "Call (TCO)" } else { "Call" };
self.log(label, node);
BoundKind::Call { callee, args } => {
self.log("Call", node);
self.indent += 1;
self.write_indent();
+1 -1
View File
@@ -57,7 +57,7 @@ impl<'a> LambdaCollector<'a> {
self.visit(value);
}
BoundKind::Call { callee, args, .. } => {
BoundKind::Call { callee, args } => {
self.visit(callee);
self.visit(args);
}
+10 -8
View File
@@ -34,7 +34,7 @@ impl Optimizer {
fn visit_node(&self, node: TypedNode) -> TypedNode {
let (new_kind, new_ty) = match node.kind {
BoundKind::Call { callee, args, is_tail } => {
BoundKind::Call { callee, args } => {
let callee = self.visit_node(*callee);
let args = self.visit_node(*args);
@@ -52,6 +52,7 @@ impl Optimizer {
for (i, cell) in closure.upvalues.iter().enumerate() {
sub.add_upvalue(i as u32, cell.borrow().clone());
}
// DIRECT ACCESS: Use the original TypedNode from the closure
let inlined_body = sub.inline((*closure.function_node).clone());
// Try to collapse the now-stateless lambda call
@@ -73,6 +74,7 @@ impl Optimizer {
for (i, cell) in closure.upvalues.iter().enumerate() {
sub.add_upvalue(i as u32, cell.borrow().clone());
}
// DIRECT ACCESS: Use the original TypedNode
let inlined_body = sub.inline((*closure.function_node).clone());
let cracked_lambda = Node {
@@ -87,12 +89,12 @@ impl Optimizer {
};
return Node {
identity: node.identity,
kind: BoundKind::Call { callee: Box::new(cracked_lambda), args: Box::new(args), is_tail },
kind: BoundKind::Call { callee: Box::new(cracked_lambda), args: Box::new(args) },
ty: node.ty,
};
}
(BoundKind::Call { callee: Box::new(callee), args: Box::new(args), is_tail }, node.ty)
(BoundKind::Call { callee: Box::new(callee), args: Box::new(args) }, node.ty)
},
BoundKind::If { cond, then_br, else_br } => {
@@ -269,7 +271,7 @@ impl Optimizer {
self.contains_def_local(cond) || self.contains_def_local(then_br) || else_br.as_ref().is_some_and(|e| self.contains_def_local(e))
}
BoundKind::Block { exprs } => exprs.iter().any(|e| self.contains_def_local(e)),
BoundKind::Call { callee, args, .. } => self.contains_def_local(callee) || self.contains_def_local(args),
BoundKind::Call { callee, args } => self.contains_def_local(callee) || self.contains_def_local(args),
BoundKind::Lambda { .. } => false, // Nested lambda definitions are safe (they have their own frame)
BoundKind::Tuple { elements } => elements.iter().any(|e| self.contains_def_local(e)),
BoundKind::Record { fields } => fields.iter().any(|(k, v)| self.contains_def_local(k) || self.contains_def_local(v)),
@@ -388,10 +390,10 @@ impl SubstitutionMap {
let exprs = exprs.into_iter().map(|e| self.inline_recursive(e, depth, upvalue_subs)).collect();
(BoundKind::Block { exprs }, node.ty)
},
BoundKind::Call { callee, args, is_tail } => {
BoundKind::Call { callee, args } => {
let callee = Box::new(self.inline_recursive(*callee, depth, upvalue_subs));
let args = Box::new(self.inline_recursive(*args, depth, upvalue_subs));
(BoundKind::Call { callee, args, is_tail }, node.ty)
(BoundKind::Call { callee, args }, node.ty)
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.inline_recursive(*value, depth, upvalue_subs));
@@ -444,10 +446,10 @@ impl SubstitutionMap {
let exprs = exprs.into_iter().map(|e| self.reindex_upvalues(e, mapping)).collect();
(BoundKind::Block { exprs }, node.ty)
},
BoundKind::Call { callee, args, is_tail } => {
BoundKind::Call { callee, args } => {
let callee = Box::new(self.reindex_upvalues(*callee, mapping));
let args = Box::new(self.reindex_upvalues(*args, mapping));
(BoundKind::Call { callee, args, is_tail }, node.ty)
(BoundKind::Call { callee, args }, node.ty)
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
let value = Box::new(self.reindex_upvalues(*value, mapping));
+2 -2
View File
@@ -48,9 +48,9 @@ impl Specializer {
fn visit_node(&self, node: TypedNode) -> TypedNode {
let (new_kind, new_ty) = match node.kind {
BoundKind::Call { callee, args, is_tail } => {
BoundKind::Call { callee, args } => {
let (new_callee, new_args, ret_ty) = self.specialize_call_logic(*callee, *args, node.ty.clone());
(BoundKind::Call { callee: Box::new(new_callee), args: Box::new(new_args), is_tail }, ret_ty)
(BoundKind::Call { callee: Box::new(new_callee), args: Box::new(new_args) }, ret_ty)
},
// Recursive traversal for other nodes
+80 -83
View File
@@ -1,123 +1,120 @@
use std::rc::Rc;
use crate::ast::nodes::Node;
use crate::ast::compiler::bound_nodes::BoundKind;
use crate::ast::compiler::bound_nodes::{BoundKind, TypedNode};
use crate::ast::types::StaticType;
#[derive(Debug, Clone)]
pub struct RuntimeMetadata {
pub ty: StaticType,
pub is_tail: bool,
/// The original, high-level typed node. Perfect for Debuggers and Optimizers.
pub original: Rc<TypedNode>,
}
/// The ExecNode is the AST used by the VM. It carries TCO flags and links to source.
pub type ExecNode = Node<BoundKind<RuntimeMetadata>, RuntimeMetadata>;
pub struct TCO;
impl TCO {
pub fn optimize<T: Clone>(node: Node<BoundKind<T>, T>) -> Node<BoundKind<T>, T> {
// Top-level starts NOT in tail position usually, unless the script result is returned immediately
// which implies tail position for the script body if viewed as a function.
// Let's assume standard execution context (not tail call) for the script root.
Self::transform(node, false)
/// Lowers a TypedNode (Compiler-AST) to an ExecNode (VM-AST) and marks tail positions.
pub fn optimize(node: TypedNode) -> ExecNode {
Self::transform(Rc::new(node), true)
}
fn transform<T: Clone>(node: Node<BoundKind<T>, T>, is_tail_position: bool) -> Node<BoundKind<T>, T> {
match node.kind {
BoundKind::Call { callee, args, .. } => {
let new_callee = Box::new(Self::transform(*callee, false));
let new_args = Box::new(Self::transform(*args, false));
Node {
kind: BoundKind::Call {
callee: new_callee,
args: new_args,
is_tail: is_tail_position,
},
..node
fn transform(node_rc: Rc<TypedNode>, is_tail_position: bool) -> ExecNode {
let node = &*node_rc;
let new_kind = match &node.kind {
BoundKind::Call { callee, args } => {
BoundKind::Call {
callee: Box::new(Self::transform(Rc::new((**callee).clone()), false)),
args: Box::new(Self::transform(Rc::new((**args).clone()), false)),
}
},
BoundKind::If { cond, then_br, else_br } => {
let new_cond = Box::new(Self::transform(*cond, false));
let new_then = Box::new(Self::transform(*then_br, is_tail_position));
let new_else = else_br.map(|e| Box::new(Self::transform(*e, is_tail_position)));
Node {
kind: BoundKind::If {
cond: new_cond,
then_br: new_then,
else_br: new_else,
},
..node
BoundKind::If {
cond: Box::new(Self::transform(Rc::new((**cond).clone()), false)),
then_br: Box::new(Self::transform(Rc::new((**then_br).clone()), is_tail_position)),
else_br: else_br.as_ref().map(|e| Box::new(Self::transform(Rc::new((**e).clone()), is_tail_position))),
}
},
BoundKind::Block { exprs } => {
if exprs.is_empty() {
return Node {
kind: BoundKind::Block { exprs },
..node
};
}
let last_idx = exprs.len() - 1;
let mut new_exprs = Vec::with_capacity(exprs.len());
for (i, expr) in exprs.into_iter().enumerate() {
let is_last = i == last_idx;
// Only the last expression inherits the tail context
new_exprs.push(Self::transform(expr, is_tail_position && is_last));
}
Node {
kind: BoundKind::Block { exprs: new_exprs },
..node
BoundKind::Block { exprs: vec![] }
} else {
let last_idx = exprs.len() - 1;
let mut new_exprs = Vec::with_capacity(exprs.len());
for (i, expr) in exprs.iter().enumerate() {
let is_last = i == last_idx;
new_exprs.push(Self::transform(Rc::new(expr.clone()), is_tail_position && is_last));
}
BoundKind::Block { exprs: new_exprs }
}
},
BoundKind::Lambda { params, upvalues, body, positional_count } => {
// The body of a lambda is implicitly in tail position when the lambda is called.
let new_body = Rc::new(Self::transform((*body).clone(), true));
Node {
kind: BoundKind::Lambda {
params: params.clone(),
upvalues,
body: new_body,
positional_count,
},
..node
BoundKind::Lambda {
params: Rc::new(Self::transform(params.clone(), false)),
upvalues: upvalues.clone(),
body: Rc::new(Self::transform(body.clone(), true)),
positional_count: *positional_count,
}
},
BoundKind::Set { addr, value } => {
Node {
kind: BoundKind::Set { addr, value: Box::new(Self::transform(*value, false)) },
..node
}
BoundKind::Set { addr: *addr, value: Box::new(Self::transform(Rc::new((**value).clone()), false)) }
},
BoundKind::DefLocal { name, slot, value, captured_by } => {
Node {
kind: BoundKind::DefLocal { name, slot, value: Box::new(Self::transform(*value, false)), captured_by },
..node
BoundKind::DefLocal {
name: name.clone(),
slot: *slot,
value: Box::new(Self::transform(Rc::new((**value).clone()), false)),
captured_by: captured_by.clone()
}
},
BoundKind::DefGlobal { name, global_index, value } => {
Node {
kind: BoundKind::DefGlobal { name, global_index, value: Box::new(Self::transform(*value, false)) },
..node
BoundKind::DefGlobal {
name: name.clone(),
global_index: *global_index,
value: Box::new(Self::transform(Rc::new((**value).clone()), false))
}
},
BoundKind::Record { fields } => {
let new_fields = fields.into_iter().map(|(k, v)| {
(Self::transform(k, false), Self::transform(v, false))
let new_fields = fields.iter().map(|(k, v)| {
(Self::transform(Rc::new(k.clone()), false), Self::transform(Rc::new(v.clone()), false))
}).collect();
Node {
kind: BoundKind::Record { fields: new_fields },
..node
}
BoundKind::Record { fields: new_fields }
},
BoundKind::Tuple { elements } => {
let new_elements = elements.into_iter().map(|e| Self::transform(e, false)).collect();
Node {
kind: BoundKind::Tuple { elements: new_elements },
..node
}
let new_elements = elements.iter().map(|e| Self::transform(Rc::new(e.clone()), false)).collect();
BoundKind::Tuple { elements: new_elements }
},
BoundKind::Parameter { name, slot } => BoundKind::Parameter { name: name.clone(), slot: *slot },
BoundKind::Constant(v) => BoundKind::Constant(v.clone()),
BoundKind::Get { addr, name } => BoundKind::Get { addr: *addr, name: name.clone() },
BoundKind::Nop => BoundKind::Nop,
BoundKind::Expansion { original_call, bound_expanded } => {
BoundKind::Expansion {
original_call: original_call.clone(),
bound_expanded: Box::new(Self::transform(Rc::new((**bound_expanded).clone()), is_tail_position))
}
}
BoundKind::Extension(_) => {
BoundKind::Nop
}
};
Node {
identity: node.identity.clone(),
kind: new_kind,
ty: RuntimeMetadata {
ty: node.ty.clone(),
is_tail: is_tail_position,
original: node_rc,
},
// Atoms and variables don't change
_ => node,
}
}
}
+2 -3
View File
@@ -263,7 +263,7 @@ impl TypeChecker {
}, fn_ty)
},
BoundKind::Call { callee, args, is_tail } => {
BoundKind::Call { callee, args } => {
let callee_typed = self.check_node(*callee, ctx)?;
let args_typed = self.check_node(*args, ctx)?;
@@ -271,8 +271,7 @@ impl TypeChecker {
(BoundKind::Call {
callee: Box::new(callee_typed),
args: Box::new(args_typed),
is_tail
args: Box::new(args_typed)
}, ret_ty)
},