Add lambda expression support

Introduces `UntypedKind::Lambda` to represent function closures.
Adds `parse_fn` to the parser to handle lambda syntax.
Updates `Node::eval` to correctly evaluate lambda expressions, creating
new scopes for each invocation with captured variables.
Improves handling of call expressions by passing the AST node's
identity.
Removes unnecessary comments and simplifies some error handling.
This commit is contained in:
Michael Schimmel
2026-02-17 00:43:09 +01:00
parent 647fc79d28
commit 145e12b811
2 changed files with 94 additions and 53 deletions
+35 -19
View File
@@ -16,7 +16,7 @@ pub trait CustomNode: Debug + Send + Sync {
fn display_name(&self) -> &'static str;
}
/// Dynamic Scope for the interpreter (Temporary solution)
/// Dynamic Scope for the interpreter
#[derive(Debug, Clone)]
pub struct Scope {
values: HashMap<String, Value>,
@@ -54,10 +54,7 @@ pub struct Context {
impl Context {
pub fn new() -> Self {
let mut root_scope = Scope::new(None);
// Register standard library (built-ins)
register_stdlib(&mut root_scope);
Self {
scope: Arc::new(Mutex::new(root_scope)),
}
@@ -65,19 +62,15 @@ impl Context {
}
fn register_stdlib(scope: &mut Scope) {
// Helper macro to reduce boilerplate
macro_rules! bin_op {
($name:expr, $op:tt) => {
scope.define($name, Value::Function(Arc::new(|args| {
if args.len() < 2 { return Value::Void; } // Error handling later
// Fold allow multi-arg: (+ 1 2 3) -> 6
if args.len() < 2 { return Value::Void; }
let mut acc = match &args[0] {
Value::Int(i) => *i as f64,
Value::Float(f) => *f,
_ => return Value::Void,
};
for arg in &args[1..] {
let val = match arg {
Value::Int(i) => *i as f64,
@@ -86,8 +79,6 @@ fn register_stdlib(scope: &mut Scope) {
};
acc = acc $op val;
}
// Return Int if result is integer-like (simplified)
if acc.fract() == 0.0 {
Value::Int(acc as i64)
} else {
@@ -102,11 +93,9 @@ fn register_stdlib(scope: &mut Scope) {
bin_op!("*", *);
bin_op!("/", /);
// Logic
scope.define(">", Value::Function(Arc::new(|args| {
if args.len() != 2 { return Value::Void; }
let (v1, v2) = (&args[0], &args[1]);
match (v1, v2) {
match (&args[0], &args[1]) {
(Value::Int(a), Value::Int(b)) => Value::Bool(a > b),
(Value::Float(a), Value::Float(b)) => Value::Bool(a > b),
(Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) > *b),
@@ -116,11 +105,8 @@ fn register_stdlib(scope: &mut Scope) {
})));
}
/// The core AST variant enum for performance and structure
#[derive(Debug)]
pub enum UntypedKind {
/// The "..." placeholder for incomplete code
Nop,
Constant(Value),
Identifier(Arc<str>),
@@ -129,11 +115,15 @@ pub enum UntypedKind {
then_br: Box<Node<UntypedKind>>,
else_br: Option<Box<Node<UntypedKind>>>,
},
// CLOSURE SUPPORT
Lambda {
params: Vec<Arc<str>>,
body: Arc<Node<UntypedKind>>, // Arc for cheap cloning into closure
},
Call {
callee: Box<Node<UntypedKind>>,
args: Vec<Node<UntypedKind>>,
},
/// The "escape hatch" for decentralized node types
Extension(Box<dyn CustomNode>),
}
@@ -160,10 +150,36 @@ impl Node<UntypedKind> {
}
}
// --- CLOSURE IMPLEMENTATION ---
UntypedKind::Lambda { params, body } => {
let captured_scope = ctx.scope.clone();
let params = params.clone();
let body = body.clone();
Ok(Value::Function(Arc::new(move |args| {
// 1. New Scope with captured parent
let mut new_scope = Scope::new(Some(captured_scope.clone()));
// 2. Bind Args
for (i, param) in params.iter().enumerate() {
if i < args.len() {
new_scope.define(param, args[i].clone());
} else {
new_scope.define(param, Value::Void);
}
}
// 3. Eval Body
// Note: We swallow errors here because Value::Function signature is simple.
// In a full VM, we'd return Result<Value, Error>.
let mut sub_ctx = Context { scope: Arc::new(Mutex::new(new_scope)) };
body.eval(&mut sub_ctx).unwrap_or(Value::Void)
})))
},
UntypedKind::Call { callee, args } => {
let func_val = callee.eval(ctx)?;
// Evaluate all arguments first (strict evaluation)
let mut eval_args = Vec::with_capacity(args.len());
for arg in args {
eval_args.push(arg.eval(ctx)?);