Refactor Binder to use simpler types

The Binder has been refactored to simplify its internal types and reduce
redundancy.
Key changes include:
- Removed StaticType from LocalInfo, as it's not strictly needed for the
  binder.
- Simplified `define` in CompilerScope to not take a StaticType.
- Removed StaticType from upvalues in FunctionCompiler.
- Adjusted global mappings to store only the index, not the type.
- Updated BoundKind to use a generic type parameter `T` for metadata,
  defaulting to `()` for untyped bound nodes.
- Introduced `BoundNode` and `TypedNode` type aliases for clarity.
- Minor adjustments to dumper and VM to accommodate the new generic
  BoundKind.
This commit is contained in:
Michael Schimmel
2026-02-19 12:07:28 +01:00
parent c49561255e
commit 4673ea78b9
7 changed files with 130 additions and 176 deletions
+56 -134
View File
@@ -1,13 +1,16 @@
use std::collections::{HashMap, BTreeMap};
use std::collections::HashMap;
use std::rc::Rc;
use std::cell::RefCell;
use crate::ast::nodes::{Node, UntypedKind, Symbol};
use crate::ast::compiler::bound_nodes::{BoundKind, Address};
use crate::ast::types::{Identity, StaticType, Value};
use crate::ast::compiler::bound_nodes::{BoundKind, Address, BoundNode};
use crate::ast::types::{Identity, StaticType};
#[derive(Debug, Clone)]
struct LocalInfo {
slot: u32,
// Note: Binder doesn't strictly need the type anymore,
// but it might be useful for built-ins during resolution.
// For now we keep it as Any or Unknown.
ty: StaticType,
}
@@ -25,12 +28,12 @@ impl CompilerScope {
}
}
fn define(&mut self, sym: &Symbol, ty: StaticType) -> Result<u32, String> {
fn define(&mut self, sym: &Symbol) -> Result<u32, String> {
if self.locals.contains_key(sym) {
return Err(format!("Variable '{}' is already defined in this scope level.", sym.name));
}
let slot = self.slot_count;
self.locals.insert(sym.clone(), LocalInfo { slot, ty });
self.locals.insert(sym.clone(), LocalInfo { slot, ty: StaticType::Any });
self.slot_count += 1;
Ok(slot)
}
@@ -42,7 +45,7 @@ impl CompilerScope {
struct FunctionCompiler {
scope: CompilerScope,
upvalues: Vec<(Address, StaticType)>,
upvalues: Vec<Address>,
}
impl FunctionCompiler {
@@ -53,30 +56,30 @@ impl FunctionCompiler {
}
}
fn add_upvalue(&mut self, addr: Address, ty: StaticType) -> u32 {
if let Some(idx) = self.upvalues.iter().position(|(a, _)| *a == addr) {
fn add_upvalue(&mut self, addr: Address) -> u32 {
if let Some(idx) = self.upvalues.iter().position(|&a| a == addr) {
return idx as u32;
}
let idx = self.upvalues.len() as u32;
self.upvalues.push((addr, ty));
self.upvalues.push(addr);
idx
}
}
pub struct Binder {
functions: Vec<FunctionCompiler>,
// Globals mapping: Symbol -> (Index, Type)
globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>,
// Globals mapping: Symbol -> Index
globals: Rc<RefCell<HashMap<Symbol, u32>>>,
// Map of Declaration Identity -> List of Lambda Identities that capture it
capture_map: HashMap<Identity, Vec<Identity>>,
}
impl Binder {
pub fn new(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>) -> Self {
pub fn new(globals: Rc<RefCell<HashMap<Symbol, u32>>>) -> Self {
Self::with_boxed(globals, HashMap::new())
}
fn with_boxed(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, captures: HashMap<Identity, Vec<Identity>>) -> Self {
fn with_boxed(globals: Rc<RefCell<HashMap<Symbol, u32>>>, captures: HashMap<Identity, Vec<Identity>>) -> Self {
let mut binder = Self {
functions: Vec::new(),
globals,
@@ -86,52 +89,37 @@ impl Binder {
binder
}
pub fn bind_root(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
pub fn bind_root(globals: Rc<RefCell<HashMap<Symbol, u32>>>, node: &Node<UntypedKind>) -> Result<BoundNode, String> {
let captures = crate::ast::compiler::upvalues::UpvalueAnalyzer::analyze(node);
let mut binder = Self::with_boxed(globals, captures);
binder.bind(node)
}
pub fn bind(&mut self, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
pub fn bind(&mut self, node: &Node<UntypedKind>) -> Result<BoundNode, String> {
match &node.kind {
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop, StaticType::Void)),
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop)),
UntypedKind::Constant(v) => {
let ty = v.static_type();
Ok(self.make_node(node.identity.clone(), BoundKind::Constant(v.clone()), ty))
Ok(self.make_node(node.identity.clone(), BoundKind::Constant(v.clone())))
},
UntypedKind::Identifier(sym) => {
let (addr, ty) = self.resolve_variable(sym)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Get(addr), ty))
let addr = self.resolve_variable(sym)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Get(addr)))
},
UntypedKind::If { cond, then_br, else_br } => {
let cond = self.bind(cond)?;
// Type check condition: Must be Bool or Any
if cond.ty != StaticType::Bool && cond.ty != StaticType::Any {
return Err(format!("Condition must be boolean, found {:?}", cond.ty));
}
let then_br = self.bind(then_br)?;
let mut else_br_bound = None;
let final_ty = if let Some(e) = else_br {
let eb = self.bind(e)?;
let ty = if then_br.ty == eb.ty {
then_br.ty.clone()
} else {
StaticType::Any // Common supertype logic could go here
};
else_br_bound = Some(Box::new(eb));
ty
} else {
StaticType::Void // If without else returns Void if false
};
if let Some(e) = else_br {
else_br_bound = Some(Box::new(self.bind(e)?));
}
Ok(self.make_node(node.identity.clone(), BoundKind::If {
cond: Box::new(cond),
then_br: Box::new(then_br),
else_br: else_br_bound
}, final_ty))
}))
},
UntypedKind::Def { name, value } => {
@@ -142,42 +130,29 @@ impl Binder {
return Err(format!("Global variable '{}' is already defined.", name.name));
}
let idx = globals.len() as u32;
globals.insert(name.clone(), (idx, StaticType::Any));
globals.insert(name.clone(), idx);
idx
} else {
let current_fn = self.functions.last_mut().unwrap();
current_fn.scope.define(name, StaticType::Any)?
current_fn.scope.define(name)?
};
// 2. Bind Value (now 'name' is visible)
let val_node = self.bind(value)?;
let ty = val_node.ty.clone();
// 3. Update Type and Return Node
// 3. Return Node
if self.functions.len() == 1 {
{
let mut globals = self.globals.borrow_mut();
if let Some(entry) = globals.get_mut(name) {
entry.1 = ty.clone();
}
}
Ok(self.make_node(node.identity.clone(), BoundKind::DefGlobal {
global_index: slot_or_idx,
value: Box::new(val_node)
}, ty))
}))
} else {
{
let current_fn = self.functions.last_mut().unwrap();
if let Some(info) = current_fn.scope.locals.get_mut(name) {
info.ty = ty.clone();
}
}
let captured_by = self.capture_map.get(&node.identity).cloned().unwrap_or_default();
Ok(self.make_node(node.identity.clone(), BoundKind::DefLocal {
slot: slot_or_idx,
value: Box::new(val_node) ,
captured_by
}, ty))
}))
}
},
@@ -185,17 +160,11 @@ impl Binder {
let val_node = self.bind(value)?;
if let UntypedKind::Identifier(sym) = &target.kind {
let (addr, target_ty) = self.resolve_variable(sym)?;
// Type check: value must be compatible with target
if target_ty != StaticType::Any && val_node.ty != StaticType::Any && target_ty != val_node.ty {
return Err(format!("Cannot assign {:?} to variable of type {:?}", val_node.ty, target_ty));
}
let addr = self.resolve_variable(sym)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Set {
addr,
value: Box::new(val_node)
}, target_ty))
}))
} else {
Err("Assignment target must be an identifier".to_string())
}
@@ -204,65 +173,42 @@ impl Binder {
UntypedKind::Lambda { params, body } => {
self.functions.push(FunctionCompiler::new());
let mut param_types = Vec::new();
{
let current_fn = self.functions.last_mut().unwrap();
for param in params {
// Lambda params must also be unique in their scope
current_fn.scope.define(param, StaticType::Any)?;
param_types.push(StaticType::Any);
current_fn.scope.define(param)?;
}
}
let body_bound = self.bind(body)?;
let ret_ty = body_bound.ty.clone();
let compiled_fn = self.functions.pop().unwrap();
let upvalues: Vec<Address> = compiled_fn.upvalues.into_iter().map(|(a, _)| a).collect();
let fn_ty = StaticType::Function {
params: param_types,
ret: Box::new(ret_ty),
};
Ok(self.make_node(node.identity.clone(), BoundKind::Lambda {
param_count: params.len() as u32,
upvalues,
upvalues: compiled_fn.upvalues,
body: Rc::new(body_bound),
}, fn_ty))
}))
},
UntypedKind::Call { callee, args } => {
let callee = self.bind(callee)?;
let mut bound_args = Vec::new();
let mut arg_types = Vec::new();
for arg in args {
let b = self.bind(arg)?;
arg_types.push(b.ty.clone());
bound_args.push(b);
bound_args.push(self.bind(arg)?);
}
let ret_ty = match &callee.ty {
StaticType::Function { ret, .. } => *ret.clone(),
StaticType::Any => StaticType::Any,
_ => return Err(format!("Attempt to call non-function of type {:?}", callee.ty)),
};
Ok(self.make_node(node.identity.clone(), BoundKind::Call {
callee: Box::new(callee),
args: bound_args
}, ret_ty))
}))
},
UntypedKind::Block { exprs } => {
let mut bound_exprs = Vec::new();
let mut last_ty = StaticType::Void;
for expr in exprs {
let b = self.bind(expr)?;
last_ty = b.ty.clone();
bound_exprs.push(b);
bound_exprs.push(self.bind(expr)?);
}
Ok(self.make_node(node.identity.clone(), BoundKind::Block { exprs: bound_exprs }, last_ty))
Ok(self.make_node(node.identity.clone(), BoundKind::Block { exprs: bound_exprs }))
},
UntypedKind::Tuple { elements } => {
@@ -270,46 +216,23 @@ impl Binder {
for e in elements {
bound_elems.push(self.bind(e)?);
}
// For now, tuple type is List(Any)
let ty = StaticType::List(Box::new(StaticType::Any));
Ok(self.make_node(node.identity.clone(), BoundKind::Tuple { elements: bound_elems }, ty))
Ok(self.make_node(node.identity.clone(), BoundKind::Tuple { elements: bound_elems }))
},
UntypedKind::Map { entries } => {
let mut bound_entries = Vec::new();
let mut key_types = BTreeMap::new(); // For Record type inference if keys are constant keywords
for (k, v) in entries {
// Keys must be compile-time constants for now (for Record type),
// or at least we enforce keywords.
// But `bind` processes runtime expressions too.
// Let's bind both.
let bound_k = self.bind(k)?;
let bound_v = self.bind(v)?;
// If key is a constant keyword, we can build a static record type.
if let BoundKind::Constant(Value::Keyword(kw)) = &bound_k.kind {
key_types.insert(*kw, bound_v.ty.clone());
}
bound_entries.push((bound_k, bound_v));
bound_entries.push((self.bind(k)?, self.bind(v)?));
}
// If all keys are known, we produce a Record type. Else Map(Any, Any) which we don't have yet.
// We default to Record(Any) if keys are dynamic (not supported well yet) or known.
// Since our parser enforced keywords, we assume we have a Record.
let ty = StaticType::Record(Rc::new(key_types));
Ok(self.make_node(node.identity.clone(), BoundKind::Map { entries: bound_entries }, ty))
Ok(self.make_node(node.identity.clone(), BoundKind::Map { entries: bound_entries }))
},
UntypedKind::Expansion { call, expanded } => {
let bound_expanded = self.bind(expanded)?;
let ty = bound_expanded.ty.clone();
Ok(self.make_node(node.identity.clone(), BoundKind::Expansion {
original_call: Rc::from(call.as_ref().clone()),
bound_expanded: Box::new(bound_expanded),
}, ty))
}))
},
UntypedKind::Template(_) | UntypedKind::Placeholder(_) | UntypedKind::Splice(_) | UntypedKind::MacroDecl { .. } => {
@@ -317,52 +240,51 @@ impl Binder {
}
UntypedKind::Extension(_) => {
// Future: Delegate to extension binder
Err("Custom extensions not supported in Binder yet".to_string())
}
}
}
fn resolve_variable(&mut self, sym: &Symbol) -> Result<(Address, StaticType), String> {
fn resolve_variable(&mut self, sym: &Symbol) -> Result<Address, String> {
let current_fn_idx = self.functions.len() - 1;
// 1. Try local in current function
if let Some(info) = self.functions[current_fn_idx].scope.resolve(sym) {
return Ok((Address::Local(info.slot), info.ty));
return Ok(Address::Local(info.slot));
}
// 2. Try enclosing scopes (capture chain)
for i in (0..current_fn_idx).rev() {
if let Some(info) = self.functions[i].scope.resolve(sym) {
let mut addr = Address::Local(info.slot);
let ty = info.ty.clone();
for k in (i + 1)..=current_fn_idx {
addr = Address::Upvalue(self.functions[k].add_upvalue(addr, ty.clone()));
addr = Address::Upvalue(self.functions[k].add_upvalue(addr));
}
return Ok((addr, ty));
return Ok(addr);
}
}
// 3. Try Global
let globals = self.globals.borrow();
if let Some((idx, ty)) = globals.get(sym) {
return Ok((Address::Global(*idx), ty.clone()));
if let Some(idx) = globals.get(sym) {
return Ok(Address::Global(*idx));
}
// 4. Global Fallback: If not found with context, try without context
// (Allows macros to access global built-ins like *, +, etc.)
// 4. Global Fallback
if sym.context.is_some() {
let fallback_sym = Symbol { name: sym.name.clone(), context: None };
if let Some((idx, ty)) = globals.get(&fallback_sym) {
return Ok((Address::Global(*idx), ty.clone()));
if let Some(idx) = globals.get(&fallback_sym) {
return Ok(Address::Global(*idx));
}
}
Err(format!("Undefined variable '{}'", sym.name))
}
fn make_node(&self, identity: Identity, kind: BoundKind, ty: StaticType) -> Node<BoundKind, StaticType> {
Node { identity, kind, ty }
fn make_node(&self, identity: Identity, kind: BoundKind<()>) -> BoundNode {
Node { identity, kind, ty: () }
}
}
+42 -19
View File
@@ -2,15 +2,27 @@ use std::rc::Rc;
use crate::ast::types::{Value, StaticType, Identity};
use crate::ast::nodes::Node;
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Address {
Local(u32), // Stack-Slot index (relative to frame base)
Upvalue(u32), // Index in the closure's upvalue array
Global(u32), // Index in the global environment vector
}
/// Trait for DSL-specific AST extensions (like Pipe, Series-Access, etc.)
pub trait BoundExtension<T>: std::fmt::Debug {
fn clone_box(&self) -> Box<dyn BoundExtension<T>>;
fn display_name(&self) -> String;
}
impl<T> Clone for Box<dyn BoundExtension<T>> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Debug, Clone)]
pub enum BoundKind {
pub enum BoundKind<T = ()> {
Nop,
Constant(Value),
@@ -20,66 +32,76 @@ pub enum BoundKind {
// Variable Update (Assignment)
Set {
addr: Address,
value: Box<Node<BoundKind, StaticType>>,
value: Box<Node<BoundKind<T>, T>>,
},
// Variable Declaration (Local)
DefLocal {
slot: u32,
value: Box<Node<BoundKind, StaticType>>,
value: Box<Node<BoundKind<T>, T>>,
captured_by: Vec<Identity>,
},
If {
cond: Box<Node<BoundKind, StaticType>>,
then_br: Box<Node<BoundKind, StaticType>>,
else_br: Option<Box<Node<BoundKind, StaticType>>>,
cond: Box<Node<BoundKind<T>, T>>,
then_br: Box<Node<BoundKind<T>, T>>,
else_br: Option<Box<Node<BoundKind<T>, T>>>,
},
// Global Definition (Locals are implicit by stack position)
DefGlobal {
global_index: u32,
value: Box<Node<BoundKind, StaticType>>,
value: Box<Node<BoundKind<T>, T>>,
},
Lambda {
param_count: u32,
// The list of variables captured from enclosing scopes
upvalues: Vec<Address>,
body: Rc<Node<BoundKind, StaticType>>,
body: Rc<Node<BoundKind<T>, T>>,
},
Call {
callee: Box<Node<BoundKind, StaticType>>,
args: Vec<Node<BoundKind, StaticType>>,
callee: Box<Node<BoundKind<T>, T>>,
args: Vec<Node<BoundKind<T>, T>>,
},
TailCall {
callee: Box<Node<BoundKind, StaticType>>,
args: Vec<Node<BoundKind, StaticType>>,
callee: Box<Node<BoundKind<T>, T>>,
args: Vec<Node<BoundKind<T>, T>>,
},
Block {
exprs: Vec<Node<BoundKind, StaticType>>,
exprs: Vec<Node<BoundKind<T>, T>>,
},
// NEW
Tuple {
elements: Vec<Node<BoundKind, StaticType>>,
elements: Vec<Node<BoundKind<T>, T>>,
},
Map {
entries: Vec<(Node<BoundKind, StaticType>, Node<BoundKind, StaticType>)>,
entries: Vec<(Node<BoundKind<T>, T>, Node<BoundKind<T>, T>)>,
},
/// An expanded macro call, preserving the original call for debugging and UI.
Expansion {
/// The original call from the untyped AST.
original_call: Rc<Node<crate::ast::nodes::UntypedKind>>,
/// The result of binding the expanded AST.
bound_expanded: Box<Node<BoundKind, StaticType>>,
bound_expanded: Box<Node<BoundKind<T>, T>>,
},
/// DSL-specific extension slot
Extension(Box<dyn BoundExtension<T>>),
}
impl BoundKind {
/// Type alias for a node that has been bound (addresses resolved) but not yet type-checked.
pub type BoundNode = Node<BoundKind<()>, ()>;
/// Type alias for a node that has been fully type-checked and is ready for execution.
pub type TypedNode = Node<BoundKind<StaticType>, StaticType>;
impl<T> BoundKind<T> {
pub fn display_name(&self) -> String {
match self {
BoundKind::Nop => "NOP".to_string(),
@@ -96,6 +118,7 @@ impl BoundKind {
BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
BoundKind::Map { entries } => format!("MAP({})", entries.len()),
BoundKind::Expansion { .. } => "EXPANSION".to_string(),
BoundKind::Extension(ext) => ext.display_name(),
}
}
}
+9 -5
View File
@@ -1,6 +1,6 @@
use crate::ast::nodes::Node;
use crate::ast::compiler::bound_nodes::BoundKind;
use crate::ast::types::StaticType;
use std::fmt::Debug;
/// Human-readable AST dumper for the bound AST.
pub struct Dumper {
@@ -10,7 +10,7 @@ pub struct Dumper {
impl Dumper {
/// Produces a formatted string representation of the given bound AST node and its children.
pub fn dump(node: &Node<BoundKind, StaticType>) -> String {
pub fn dump<T: Debug>(node: &Node<BoundKind<T>, T>) -> String {
let mut dumper = Self {
output: String::new(),
indent: 0,
@@ -25,13 +25,13 @@ impl Dumper {
}
}
fn log(&mut self, label: &str, node: &Node<BoundKind, StaticType>) {
fn log<T: Debug>(&mut self, label: &str, node: &Node<BoundKind<T>, T>) {
self.write_indent();
self.output.push_str(label);
self.output.push_str(&format!(" <Type: {:?}>\n", node.ty));
self.output.push_str(&format!(" <Metadata: {:?}>\n", node.ty));
}
fn visit(&mut self, node: &Node<BoundKind, StaticType>) {
fn visit<T: Debug>(&mut self, node: &Node<BoundKind<T>, T>) {
match &node.kind {
BoundKind::Nop => self.log("Nop", node),
BoundKind::Constant(v) => self.log(&format!("Constant: {}", v), node),
@@ -156,6 +156,10 @@ impl Dumper {
self.visit(bound_expanded);
self.indent -= 1;
}
BoundKind::Extension(ext) => {
self.log(&ext.display_name(), node);
}
}
}
}
+1 -1
View File
@@ -547,7 +547,7 @@ mod tests {
let expanded = expander.expand(untyped).unwrap();
let mut global_names = HashMap::new();
global_names.insert(Symbol::from("*"), (0, StaticType::Any));
global_names.insert(Symbol::from("*"), 0);
let globals = Rc::new(RefCell::new(global_names));
let result = Binder::bind_root(globals, &expanded);
+2 -5
View File
@@ -1,19 +1,18 @@
use std::rc::Rc;
use crate::ast::nodes::Node;
use crate::ast::compiler::bound_nodes::BoundKind;
use crate::ast::types::StaticType;
pub struct TCO;
impl TCO {
pub fn optimize(node: Node<BoundKind, StaticType>) -> Node<BoundKind, StaticType> {
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)
}
fn transform(node: Node<BoundKind, StaticType>, is_tail_position: bool) -> Node<BoundKind, StaticType> {
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));
@@ -78,8 +77,6 @@ impl TCO {
BoundKind::Lambda { param_count, upvalues, body } => {
// The body of a lambda is implicitly in tail position when the lambda is called.
// However, we process the lambda definition here.
// The body node inside needs to be transformed assuming it IS in tail position relative to the function.
let new_body = Rc::new(Self::transform((*body).clone(), true));
Node {
+15 -10
View File
@@ -2,13 +2,13 @@ use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;
use std::any::Any;
use crate::ast::compiler::bound_nodes::{Address, BoundKind};
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::nodes::Node;
use crate::ast::types::{Value, Object, StaticType};
#[derive(Debug, Clone)]
pub struct Closure {
pub function_node: Rc<Node<BoundKind, StaticType>>,
pub function_node: Rc<TypedNode>,
pub upvalues: Vec<Rc<RefCell<Value>>>,
}
@@ -30,8 +30,8 @@ struct CallFrame {
/// Hook for observing VM execution (zero-cost when O::ACTIVE is false)
pub trait VMObserver {
const ACTIVE: bool = false;
fn before_eval(&mut self, _vm: &VM, _node: &Node<BoundKind, StaticType>) {}
fn after_eval(&mut self, _vm: &VM, _node: &Node<BoundKind, StaticType>, _res: &Result<Value, String>) {}
fn before_eval(&mut self, _vm: &VM, _node: &TypedNode) {}
fn after_eval(&mut self, _vm: &VM, _node: &TypedNode, _res: &Result<Value, String>) {}
}
/// Default observer that does nothing and should be optimized away
@@ -63,13 +63,13 @@ impl Default for TracingObserver {
impl VMObserver for TracingObserver {
const ACTIVE: bool = true;
fn before_eval(&mut self, _vm: &VM, node: &Node<BoundKind, StaticType>) {
fn before_eval(&mut self, _vm: &VM, node: &TypedNode) {
let pad = self.pad();
self.logs.push(format!("{}{} [{}]: {{", pad, node.kind.display_name(), node.ty));
self.indent += 1;
}
fn after_eval(&mut self, vm: &VM, node: &Node<BoundKind, StaticType>, res: &Result<Value, String>) {
fn after_eval(&mut self, vm: &VM, node: &TypedNode, res: &Result<Value, String>) {
self.indent = self.indent.saturating_sub(1);
let pad = self.pad();
@@ -262,6 +262,11 @@ macro_rules! dispatch_eval {
BoundKind::Expansion { original_call: _, bound_expanded } => {
$self.$eval_method($($observer,)? bound_expanded)
}
BoundKind::Extension(ext) => {
// Future: Delegate execution to extension
Err(format!("Execution of extension '{}' not implemented yet", ext.display_name()))
}
}
};
}
@@ -275,7 +280,7 @@ impl VM {
}
}
pub fn run(&mut self, root: &Node<BoundKind, StaticType>) -> Result<Value, String> {
pub fn run(&mut self, root: &TypedNode) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
@@ -290,7 +295,7 @@ impl VM {
result
}
pub fn run_with_observer<O: VMObserver>(&mut self, observer: &mut O, root: &Node<BoundKind, StaticType>) -> Result<Value, String> {
pub fn run_with_observer<O: VMObserver>(&mut self, observer: &mut O, root: &TypedNode) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
@@ -307,12 +312,12 @@ impl VM {
/// The pure, original, zero-cost hot path.
#[inline(always)]
fn eval(&mut self, node: &Node<BoundKind, StaticType>) -> Result<Value, String> {
fn eval(&mut self, node: &TypedNode) -> Result<Value, String> {
dispatch_eval!(self, node, eval)
}
/// The observed path for debugging.
fn eval_observed<O: VMObserver>(&mut self, observer: &mut O, node: &Node<BoundKind, StaticType>) -> Result<Value, String> {
fn eval_observed<O: VMObserver>(&mut self, observer: &mut O, node: &TypedNode) -> Result<Value, String> {
observer.before_eval(self, node);
// Wrap in a closure to ensure after_eval is called even on early returns (e.g. from ?)
+4 -1
View File
@@ -80,9 +80,12 @@ mod tests {
let mut binder = Binder::new(globals.clone());
let bound_ast = binder.bind(&ast).expect("Failed to bind");
// BRIDGE: Binder -> VM requires a TypedNode
let typed_ast = crate::ast::environment::dummy_type_check(bound_ast);
let vm_globals = Rc::new(RefCell::new(Vec::new()));
let mut vm = VM::new(vm_globals);
let result = vm.run(&bound_ast);
let result = vm.run(&typed_ast);
match result {
Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"),