Extract calculate_stack_size to a standalone function

The `calculate_stack_size` method was duplicated in `bound_nodes.rs` and
`tco.rs`. This commit extracts it into a single standalone function in
`tco.rs` to avoid duplication. The stack size is now calculated and
stored in the `ExecNode`'s `ty` field during the TCO optimization phase.
This commit is contained in:
Michael Schimmel
2026-03-11 10:35:18 +01:00
parent 3657f19047
commit db26719cad
4 changed files with 106 additions and 119 deletions
-93
View File
@@ -304,99 +304,6 @@ where
/// A single field in a Record literal (Key-Value pair)
pub type RecordField<T> = (BoundNode<T>, BoundNode<T>);
impl<T> BoundNode<T> {
pub fn calculate_stack_size(&self) -> u32 {
let mut max_slot = -1i32;
fn visit<T>(node: &BoundNode<T>, max_slot: &mut i32) {
match &node.kind {
BoundKind::Get {
addr: Address::Local(slot),
..
}
| BoundKind::Set {
addr: Address::Local(slot),
..
}
| BoundKind::Define {
addr: Address::Local(slot),
..
} => {
if slot.0 as i32 > *max_slot {
*max_slot = slot.0 as i32;
}
}
_ => {}
}
match &node.kind {
BoundKind::If {
cond,
then_br,
else_br,
} => {
visit(cond, max_slot);
visit(then_br, max_slot);
if let Some(e) = else_br {
visit(e, max_slot);
}
}
BoundKind::Set { value, .. } => {
visit(value, max_slot);
}
BoundKind::Define { value, .. } => {
visit(value, max_slot);
}
BoundKind::Destructure { pattern, value } => {
visit(pattern, max_slot);
visit(value, max_slot);
}
BoundKind::Pipe { inputs, lambda, .. } => {
for i in inputs {
visit(i, max_slot);
}
visit(lambda, max_slot);
}
BoundKind::Call { callee, args } => {
visit(callee, max_slot);
visit(args, max_slot);
}
BoundKind::Again { args } => visit(args, max_slot),
BoundKind::Block { exprs } => {
for e in exprs {
visit(e, max_slot);
}
}
BoundKind::Tuple { elements } => {
for e in elements {
visit(e, max_slot);
}
}
BoundKind::Record { values, .. } => {
for v in values {
visit(v, max_slot);
}
}
BoundKind::Expansion {
bound_expanded, ..
} => visit(bound_expanded, max_slot),
BoundKind::Lambda { params, body, .. } => {
// Check parameters and body of the lambda,
// but do NOT recurse into nested lambdas (which is handled by the generic recursion blocker below).
visit(params, max_slot);
visit(body, max_slot);
}
_ => {}
}
}
// Handle the root node: if it's a lambda, we want to check its content.
// If we just called visit(self), it would hit the Lambda case.
visit(self, &mut max_slot);
(max_slot + 1) as u32
}
}
impl<T> BoundKind<T> {
pub fn display_name(&self) -> String {
match self {
+96 -8
View File
@@ -1,4 +1,4 @@
use crate::ast::compiler::bound_nodes::{AnalyzedNode, BoundKind};
use crate::ast::compiler::bound_nodes::{Address, AnalyzedNode, BoundKind};
use crate::ast::nodes::Node;
use crate::ast::types::StaticType;
use std::fmt::Debug;
@@ -26,12 +26,105 @@ impl Debug for RuntimeMetadata {
/// The ExecNode is the AST used by the VM. It carries TCO flags and links to metrics.
pub type ExecNode = Node<BoundKind<RuntimeMetadata>, RuntimeMetadata>;
fn calc_stack_size<T>(root_node: &Node<BoundKind<T>, T>) -> u32 {
let mut max_slot = -1i32;
fn visit<T>(node: &Node<BoundKind<T>, T>, max_slot: &mut i32) {
match &node.kind {
BoundKind::Get {
addr: Address::Local(slot),
..
}
| BoundKind::Set {
addr: Address::Local(slot),
..
}
| BoundKind::Define {
addr: Address::Local(slot),
..
} => {
if slot.0 as i32 > *max_slot {
*max_slot = slot.0 as i32;
}
}
BoundKind::Lambda { .. } => {
// Do NOT recurse into nested lambdas to prevent over-allocating outer stacks
}
_ => {}
}
// Generic traversal
match &node.kind {
BoundKind::If {
cond,
then_br,
else_br,
} => {
visit(cond, max_slot);
visit(then_br, max_slot);
if let Some(e) = else_br {
visit(e, max_slot);
}
}
BoundKind::Set { value, .. } | BoundKind::Define { value, .. } => {
visit(value, max_slot);
}
BoundKind::Destructure { pattern, value } => {
visit(pattern, max_slot);
visit(value, max_slot);
}
BoundKind::Pipe { inputs, lambda, .. } => {
for i in inputs {
visit(i, max_slot);
}
visit(lambda, max_slot);
}
BoundKind::Call { callee, args } => {
visit(callee, max_slot);
visit(args, max_slot);
}
BoundKind::Again { args } => visit(args, max_slot),
BoundKind::Block { exprs } => {
for e in exprs {
visit(e, max_slot);
}
}
BoundKind::Tuple { elements } => {
for e in elements {
visit(e, max_slot);
}
}
BoundKind::Record { values, .. } => {
for v in values {
visit(v, max_slot);
}
}
BoundKind::Expansion { bound_expanded, .. } => visit(bound_expanded, max_slot),
_ => {}
}
}
// Special case for root: if the node itself is a lambda, we DO want to visit its params and body,
// but not any deeply nested lambdas.
if let BoundKind::Lambda { params, body, .. } = &root_node.kind {
visit(params, &mut max_slot);
visit(body, &mut max_slot);
} else {
visit(root_node, &mut max_slot);
}
(max_slot + 1) as u32
}
pub struct TCO;
impl TCO {
/// Lowers an AnalyzedNode to an ExecNode and marks tail positions.
pub fn optimize(node: AnalyzedNode) -> ExecNode {
Self::transform(Rc::new(node), true)
let root_stack_size = calc_stack_size(&node);
let mut exec_node = Self::transform(Rc::new(node), true);
exec_node.ty.stack_size = root_stack_size;
exec_node
}
fn transform(node_rc: Rc<AnalyzedNode>, is_tail_position: bool) -> ExecNode {
@@ -177,12 +270,7 @@ impl TCO {
};
let stack_size = match &new_kind {
BoundKind::Lambda { .. } => {
// Pre-calculate stack size for the lambda body.
// Note: 'node' here is AnalyzedNode (Node<BoundKind<NodeMetrics>, NodeMetrics>)
// 'calculate_stack_size' works for any T.
node.calculate_stack_size()
}
BoundKind::Lambda { .. } => calc_stack_size(node),
_ => 0,
};
+7 -14
View File
@@ -144,8 +144,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator {
let exec_ast = TCO::optimize(Analyzer::analyze(&typed_ast, &HashMap::new())); // Minimal analysis for macro eval
let mut vm = VM::new(self.global_values.clone());
let stack_size = exec_ast.calculate_stack_size();
vm.run(&exec_ast, stack_size)
vm.run(&exec_ast)
}
}
@@ -601,7 +600,7 @@ impl Environment {
} = &node.kind
&& upvalues.is_empty()
{
let stack_size = node.calculate_stack_size();
let stack_size = node.ty.stack_size;
let closure = Rc::new(crate::ast::vm::Closure::new(
params.ty.original.clone(),
body.ty.original.clone(),
@@ -629,7 +628,7 @@ impl Environment {
purity: Purity::Impure,
func: Rc::new(move |args| {
let mut vm = VM::new(global_values.clone());
let res = match vm.run(&exec_node, 0) { // Root call, use 0 if node is not a lambda
let res = match vm.run(&exec_node) {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e),
};
@@ -708,8 +707,7 @@ impl Environment {
let tco_ast = TCO::optimize(optimized_ast);
let mut vm = VM::new(global_values.clone());
let stack_size = tco_ast.calculate_stack_size();
let compiled_val = match vm.run(&tco_ast, stack_size) {
let compiled_val = match vm.run(&tco_ast) {
Ok(v) => v,
Err(e) => return Err(format!("VM Error during specialization: {}", e)),
};
@@ -746,7 +744,6 @@ impl Environment {
pub fn run_script_compiled(&self, compiled: TypedNode) -> Result<Value, String> {
let linked = self.link(compiled);
let _stack_size = linked.calculate_stack_size();
let func = self.instantiate(linked);
let res = (func.func)(&[]);
self.run_pipeline();
@@ -757,16 +754,12 @@ impl Environment {
self.preload_dependencies(source, None)?;
let compiled = self.compile(source).into_result()?;
let linked = self.link(compiled);
// Late Counting: Determine stack size after all optimizations are finished.
// This keeps AST nodes clean and ensures the VM always has enough space.
let stack_size = linked.calculate_stack_size();
let mut vm = VM::new(self.global_values.clone());
let mut observer = TracingObserver::new();
// 1. Run the script wrapper (returns a closure representing the script)
let result = vm.run_with_observer(&mut observer, &linked, stack_size);
// 1. Run the script wrapper (returns a closure representing the script)
let result = vm.run_with_observer(&mut observer, &linked);
// 2. Execute the root closure immediately to get the actual script result.
// All Myc scripts are wrapped in a parameterless lambda for consistency.
let mut final_result = result;
+3 -4
View File
@@ -141,8 +141,8 @@ impl VM {
}
}
pub fn run(&mut self, root: &ExecNode, stack_size: u32) -> Result<Value, String> {
self.run_with_observer(&mut NoOpObserver, root, stack_size)
pub fn run(&mut self, root: &ExecNode) -> Result<Value, String> {
self.run_with_observer(&mut NoOpObserver, root)
}
pub fn resolve_tail_calls<O: VMObserver>(
@@ -263,12 +263,11 @@ impl VM {
&mut self,
observer: &mut O,
root: &ExecNode,
stack_size: u32,
) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
self.stack.resize(stack_size as usize, Value::Void);
self.stack.resize(root.ty.stack_size as usize, Value::Void);
self.frames.push(CallFrame {
stack_base: 0,