From 0a50facab9458a59c49b5765eb4c480aee7566e4 Mon Sep 17 00:00:00 2001 From: Muad'Dib Date: Sat, 6 Jun 2026 06:33:32 +0200 Subject: [PATCH] fix(ergotree-interpreter): lambdas capture their defining environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JVM's FuncValue.eval returns a closure over the env it was created in (values.scala): each application evaluates the body in that captured env extended with the argument binding. sigma-rust's Value::Lambda carried only args + body, and every invocation site (Apply and the HOF evals) bound arguments into the CALLER's env with a save/restore dance — correct for lambdas applied where they were created, wrong for lambdas that escape: the inner lambda of add = (a: Int) => (b: Int) => a + b lost a the moment the outer Apply returned, so the curried add(3)(1) errored where the JVM evaluates 4. Lambda gains the captured bindings (snapshotted by FuncValue::eval), and a shared LambdaInvoker materializes the captured env once per lambda value and binds the argument slot per call — replacing the save/restore dance in Apply, Coll.{map,filter,fold,exists,forAll,flatMap} and Option.{map,filter}. The capture must hold uniformly: an escaped lambda fed to a HOF (val f = mk(3); coll.map(f)) hits the same root cause. Surfaced by SANTA's v6 HOF vectors (HOF_currying_Apply_of_Apply, blessed Int 4). Co-Authored-By: Claude Opus 4.8 --- ergotree-interpreter/src/eval.rs | 38 +++++ ergotree-interpreter/src/eval/apply.rs | 170 ++++++++++++++++--- ergotree-interpreter/src/eval/coll_exists.rs | 28 ++- ergotree-interpreter/src/eval/coll_filter.rs | 28 ++- ergotree-interpreter/src/eval/coll_fold.rs | 28 ++- ergotree-interpreter/src/eval/coll_forall.rs | 28 ++- ergotree-interpreter/src/eval/coll_map.rs | 28 ++- ergotree-interpreter/src/eval/env.rs | 6 + ergotree-interpreter/src/eval/func_value.rs | 11 +- ergotree-interpreter/src/eval/scoll.rs | 25 +-- ergotree-interpreter/src/eval/soption.rs | 49 ++---- ergotree-ir/src/mir/value.rs | 21 ++- 12 files changed, 310 insertions(+), 150 deletions(-) diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs index 356a48261..f24193b85 100644 --- a/ergotree-interpreter/src/eval.rs +++ b/ergotree-interpreter/src/eval.rs @@ -8,6 +8,7 @@ use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProp; use snumeric::numeric_method_evalfn; use ergotree_ir::mir::expr::Expr; +use ergotree_ir::mir::value::Lambda; use ergotree_ir::mir::value::Value; use ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean; @@ -205,6 +206,43 @@ pub(crate) trait Evaluable { ) -> Result, EvalError>; } +/// Per-lambda invoker mirroring the JVM's closure semantics: `FuncValue.eval` +/// returns a closure over the *defining* environment, and each application +/// evaluates the body in that captured env extended with the argument +/// bindings (`env1 = env + (argId -> value)`) — the caller's environment at +/// application time plays no role. +/// +/// The captured base is materialized once per lambda value; each `invoke` +/// overwrites the argument slot(s), which matches Scala's per-call extension +/// because an argument binding always shadows a same-id captured binding. +pub(crate) struct LambdaInvoker<'l, 'ctx> { + lambda: &'l Lambda<'ctx>, + env: Env<'ctx>, +} + +impl<'l, 'ctx> LambdaInvoker<'l, 'ctx> { + pub(crate) fn new(lambda: &'l Lambda<'ctx>) -> Self { + let mut env = Env::empty(); + for (idx, v) in &lambda.captured { + env.insert(*idx, v.clone()); + } + Self { lambda, env } + } + + /// Bind `args` positionally to the lambda's parameters and evaluate the + /// body in the captured environment. + pub(crate) fn invoke( + &mut self, + ctx: &Context<'ctx>, + args: Vec>, + ) -> Result, EvalError> { + for (arg, v) in self.lambda.args.iter().zip(args) { + self.env.insert(arg.idx, v); + } + self.lambda.body.eval(&mut self.env, ctx) + } +} + type EvalFn = for<'ctx> fn( mc: &SMethod, env: &mut Env<'ctx>, diff --git a/ergotree-interpreter/src/eval/apply.rs b/ergotree-interpreter/src/eval/apply.rs index 9c522fd60..93e425695 100644 --- a/ergotree-interpreter/src/eval/apply.rs +++ b/ergotree-interpreter/src/eval/apply.rs @@ -1,13 +1,12 @@ use alloc::vec::Vec; use ergotree_ir::mir::apply::Apply; -use ergotree_ir::mir::val_def::ValId; use ergotree_ir::mir::value::Value; -use hashbrown::HashMap; use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for Apply { fn eval<'ctx>( @@ -23,28 +22,13 @@ impl Evaluable for Apply { .collect::>()?; match func_v { Value::Lambda(fv) => { - let arg_ids: Vec = fv.args.iter().map(|a| a.idx).collect(); - let mut existing_variables = HashMap::new(); - let mut new_variables = vec![]; - arg_ids.iter().zip(args_v).for_each(|(idx, arg_v)| { - if let Some(old_val) = env.get(*idx) { - existing_variables.insert(idx, old_val.clone()); - } else { - new_variables.push(*idx); - } - env.insert(*idx, arg_v); - }); - let res = fv.body.eval(env, ctx); - new_variables.into_iter().for_each(|idx| { - env.remove(&idx); - }); - existing_variables - .into_iter() - .for_each(|(idx, orig_value)| { - env.insert(*idx, orig_value); - }); - - res + // The body evaluates in the lambda's CAPTURED environment + // (extended with the argument bindings) — not in the caller's + // env. Mirrors the JVM, where `FuncValue.eval` returns a + // closure over the defining env; the previous bind-into-the- + // caller's-env dance lost the outer binding of a curried + // lambda (`add(3)(1)`) as soon as the outer `Apply` returned. + LambdaInvoker::new(&fv).invoke(ctx, args_v) } _ => Err(EvalError::UnexpectedValue(format!( "expected func_v to be Value::FuncValue got: {0:?}", @@ -72,6 +56,144 @@ mod tests { use super::*; + // SANTA HOF regression (`HOF_currying_Apply_of_Apply` vector): a lambda + // returned from another lambda must capture its defining environment — + // JVM `FuncValue.eval` returns a closure over the env it was created in. + // Pre-fix, `Apply` bound arguments into the CALLER's env and removed them + // after the body ran, so the inner lambda of + // `add = (a: Int) => (b: Int) => a + b` lost `a` the moment the outer + // Apply returned, and `add(3)(1)` errored instead of evaluating to 4. + #[test] + fn curried_lambda_captures_environment() { + use crate::eval::test_util::try_eval_out_with_version; + use ergotree_ir::chain::context::Context; + use ergotree_ir::ergo_tree::ErgoTree; + use ergotree_ir::serialization::SigmaSerializable; + use sigma_test_util::force_any_val; + + fn hx(s: &str) -> alloc::vec::Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() + } + // { val add = {(a: Int) => {(b: Int) => a + b}}; add(3)(1) } — the + // vector tree (v3, segregated, sized). Clear the size bit and drop + // the size byte: the eval-tier corpus carries an Int-typed root, + // which the sized parse path rejects. + let bytes = { + let b = hx("1b200204060402d801d601d9010204d90103049a72027203dada7201017300017301"); + let mut out = alloc::vec::Vec::with_capacity(b.len() - 1); + out.push(b[0] & !0x08); + out.extend_from_slice(&b[2..]); + out + }; + let tree = ErgoTree::sigma_parse_bytes(&bytes).unwrap(); + let expr = tree.proposition().unwrap(); + let ctx = force_any_val::(); + assert_eq!( + try_eval_out_with_version::(&expr, &ctx, 3, 3).unwrap(), + 4, + "add(3)(1) must evaluate to 4 — the returned lambda must keep its captured `a`" + ); + } + + // The capture must hold uniformly across invocation sites: a lambda that + // escapes its creation site and is then fed to a HOF must still see its + // captured bindings. `{ val mk = (a: Int) => (b: Int) => a + b; + // val f = mk(3); Coll(1, 2).map(f) }` → [4, 5]. Pre-fix, `Coll.map` + // bound the argument into the caller's env, where `a` no longer existed. + #[test] + fn escaped_lambda_into_hof_keeps_captured_env() { + use ergotree_ir::mir::apply::Apply; + use ergotree_ir::mir::bin_op::ArithOp; + use ergotree_ir::mir::coll_map::Map; + use ergotree_ir::mir::collection::Collection; + + let inner_lambda: Expr = FuncValue::new( + vec![FuncArg { + idx: 3.into(), + tpe: SType::SInt, + }], + Expr::BinOp( + BinOp { + kind: ArithOp::Plus.into(), + left: Box::new( + ValUse { + val_id: 2.into(), + tpe: SType::SInt, + } + .into(), + ), + right: Box::new( + ValUse { + val_id: 3.into(), + tpe: SType::SInt, + } + .into(), + ), + } + .into(), + ), + ) + .into(); + let mk: Expr = FuncValue::new( + vec![FuncArg { + idx: 2.into(), + tpe: SType::SInt, + }], + inner_lambda, + ) + .into(); + let f: Expr = Apply::new( + ValUse { + val_id: 1.into(), + tpe: mk.tpe(), + } + .into(), + vec![Expr::Const(3i32.into())], + ) + .unwrap() + .into(); + let block: Expr = BlockValue { + items: vec![ + ValDef { + id: 1.into(), + rhs: Box::new(mk), + } + .into(), + ValDef { + id: 4.into(), + rhs: Box::new(f.clone()), + } + .into(), + ], + result: Box::new( + Map::new( + Collection::new( + SType::SInt, + vec![Expr::Const(1i32.into()), Expr::Const(2i32.into())], + ) + .unwrap() + .into(), + ValUse { + val_id: 4.into(), + tpe: f.tpe(), + } + .into(), + ) + .unwrap() + .into(), + ), + } + .into(); + assert_eq!( + eval_out_wo_ctx::>(&block), + alloc::vec![4, 5], + "the escaped lambda must keep `a = 3` when invoked by Coll.map" + ); + } + #[test] fn eval_user_defined_func_call() { let arg = Expr::Const(1i32.into()); diff --git a/ergotree-interpreter/src/eval/coll_exists.rs b/ergotree-interpreter/src/eval/coll_exists.rs index 912148fa6..03f4b06b5 100644 --- a/ergotree-interpreter/src/eval/coll_exists.rs +++ b/ergotree-interpreter/src/eval/coll_exists.rs @@ -8,6 +8,7 @@ use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for Exists { fn eval<'ctx>( @@ -18,28 +19,25 @@ impl Evaluable for Exists { let input_v = self.input.eval(env, ctx)?; let condition_v = self.condition.eval(env, ctx)?; let input_v_clone = input_v.clone(); - let mut condition_call = |arg: Value<'ctx>| match &condition_v { + let mut invoker = match &condition_v { Value::Lambda(func_value) => { - let func_arg = func_value.args.first().ok_or_else(|| { + func_value.args.first().ok_or_else(|| { EvalError::NotFound( "Exists: evaluated condition has empty arguments list".to_string(), ) })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = func_value.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res + // The body evaluates in the lambda's CAPTURED environment + // (JVM closure semantics) — not in the caller's env. + LambdaInvoker::new(func_value) + } + _ => { + return Err(EvalError::UnexpectedValue(format!( + "expected Exists::condition to be Value::FuncValue got: {0:?}", + input_v_clone + ))) } - _ => Err(EvalError::UnexpectedValue(format!( - "expected Exists::condition to be Value::FuncValue got: {0:?}", - input_v_clone - ))), }; + let mut condition_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let normalized_input_vals: Vec = match input_v { Value::Coll(coll) => { if coll.elem_tpe() != &*self.elem_tpe { diff --git a/ergotree-interpreter/src/eval/coll_filter.rs b/ergotree-interpreter/src/eval/coll_filter.rs index 4d83c8292..395874f65 100644 --- a/ergotree-interpreter/src/eval/coll_filter.rs +++ b/ergotree-interpreter/src/eval/coll_filter.rs @@ -11,6 +11,7 @@ use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for Filter { fn eval<'ctx>( @@ -21,28 +22,25 @@ impl Evaluable for Filter { let input_v = self.input.eval(env, ctx)?; let condition_v = self.condition.eval(env, ctx)?; let input_v_clone = input_v.clone(); - let mut condition_call = |arg: Value<'ctx>| match &condition_v { + let mut invoker = match &condition_v { Value::Lambda(func_value) => { - let func_arg = func_value.args.first().ok_or_else(|| { + func_value.args.first().ok_or_else(|| { EvalError::NotFound( "Filter: evaluated condition has empty arguments list".to_string(), ) })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = func_value.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res + // The body evaluates in the lambda's CAPTURED environment + // (JVM closure semantics) — not in the caller's env. + LambdaInvoker::new(func_value) + } + _ => { + return Err(EvalError::UnexpectedValue(format!( + "expected Filter::condition to be Value::FuncValue got: {0:?}", + input_v_clone + ))) } - _ => Err(EvalError::UnexpectedValue(format!( - "expected Filter::condition to be Value::FuncValue got: {0:?}", - input_v_clone - ))), }; + let mut condition_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let normalized_input_vals: Vec = match input_v { Value::Coll(coll) => { if coll.elem_tpe() != &*self.elem_tpe { diff --git a/ergotree-interpreter/src/eval/coll_fold.rs b/ergotree-interpreter/src/eval/coll_fold.rs index e327c1e16..da09cfb96 100644 --- a/ergotree-interpreter/src/eval/coll_fold.rs +++ b/ergotree-interpreter/src/eval/coll_fold.rs @@ -8,6 +8,7 @@ use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for Fold { fn eval<'ctx>( @@ -19,27 +20,24 @@ impl Evaluable for Fold { let zero_v = self.zero.eval(env, ctx)?; let fold_op_v = self.fold_op.eval(env, ctx)?; let input_v_clone = input_v.clone(); - let mut fold_op_call = |arg: Value<'ctx>| match &fold_op_v { + let mut invoker = match &fold_op_v { Value::Lambda(func_value) => { - let func_arg = func_value + func_value .args .first() .ok_or_else(|| EvalError::NotFound("empty argument for fold op".to_string()))?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = func_value.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res + // The body evaluates in the lambda's CAPTURED environment + // (JVM closure semantics) — not in the caller's env. + LambdaInvoker::new(func_value) + } + _ => { + return Err(EvalError::UnexpectedValue(format!( + "expected fold_op to be Value::FuncValue got: {0:?}", + input_v_clone + ))) } - _ => Err(EvalError::UnexpectedValue(format!( - "expected fold_op to be Value::FuncValue got: {0:?}", - input_v_clone - ))), }; + let mut fold_op_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); match input_v { Value::Coll(coll) => match coll { CollKind::NativeColl(NativeColl::CollByte(coll_byte)) => { diff --git a/ergotree-interpreter/src/eval/coll_forall.rs b/ergotree-interpreter/src/eval/coll_forall.rs index 49cce49fc..9dce1cbf7 100644 --- a/ergotree-interpreter/src/eval/coll_forall.rs +++ b/ergotree-interpreter/src/eval/coll_forall.rs @@ -8,6 +8,7 @@ use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for ForAll { fn eval<'ctx>( @@ -18,28 +19,25 @@ impl Evaluable for ForAll { let input_v = self.input.eval(env, ctx)?; let condition_v = self.condition.eval(env, ctx)?; let input_v_clone = input_v.clone(); - let mut condition_call = |arg: Value<'ctx>| match &condition_v { + let mut invoker = match &condition_v { Value::Lambda(func_value) => { - let func_arg = func_value.args.first().ok_or_else(|| { + func_value.args.first().ok_or_else(|| { EvalError::NotFound( "ForAll: evaluated condition has empty arguments list".to_string(), ) })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = func_value.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res + // The body evaluates in the lambda's CAPTURED environment + // (JVM closure semantics) — not in the caller's env. + LambdaInvoker::new(func_value) + } + _ => { + return Err(EvalError::UnexpectedValue(format!( + "expected ForAll::condition to be Value::FuncValue got: {0:?}", + input_v_clone + ))) } - _ => Err(EvalError::UnexpectedValue(format!( - "expected ForAll::condition to be Value::FuncValue got: {0:?}", - input_v_clone - ))), }; + let mut condition_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let normalized_input_vals: Vec = match input_v { Value::Coll(coll) => { if coll.elem_tpe() != &*self.elem_tpe { diff --git a/ergotree-interpreter/src/eval/coll_map.rs b/ergotree-interpreter/src/eval/coll_map.rs index f6fd5a645..ae30bbcb7 100644 --- a/ergotree-interpreter/src/eval/coll_map.rs +++ b/ergotree-interpreter/src/eval/coll_map.rs @@ -10,6 +10,7 @@ use crate::eval::env::Env; use crate::eval::Context; use crate::eval::EvalError; use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; impl Evaluable for Map { fn eval<'ctx>( @@ -20,28 +21,25 @@ impl Evaluable for Map { let input_v = self.input.eval(env, ctx)?; let mapper_v = self.mapper.eval(env, ctx)?; let input_v_clone = input_v.clone(); - let mut mapper_call = |arg: Value<'ctx>| match &mapper_v { + let mut invoker = match &mapper_v { Value::Lambda(func_value) => { - let func_arg = func_value.args.first().ok_or_else(|| { + func_value.args.first().ok_or_else(|| { EvalError::NotFound( "Map: evaluated mapper has empty arguments list".to_string(), ) })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = func_value.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res + // The body evaluates in the lambda's CAPTURED environment + // (JVM closure semantics) — not in the caller's env. + LambdaInvoker::new(func_value) + } + _ => { + return Err(EvalError::UnexpectedValue(format!( + "expected mapper to be Value::FuncValue got: {0:?}", + input_v_clone + ))) } - _ => Err(EvalError::UnexpectedValue(format!( - "expected mapper to be Value::FuncValue got: {0:?}", - input_v_clone - ))), }; + let mut mapper_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let mapper_input_tpe = self .mapper_sfunc .t_dom diff --git a/ergotree-interpreter/src/eval/env.rs b/ergotree-interpreter/src/eval/env.rs index 7fda226cc..963b89cc5 100644 --- a/ergotree-interpreter/src/eval/env.rs +++ b/ergotree-interpreter/src/eval/env.rs @@ -45,6 +45,12 @@ impl<'ctx> Env<'ctx> { pub fn get(&self, idx: ValId) -> Option<&Value<'ctx>> { self.store.get(&idx) } + + /// Snapshot the current bindings — used to capture the defining + /// environment when a `FuncValue` evaluates to a lambda value. + pub(crate) fn bindings(&self) -> Vec<(ValId, Value<'ctx>)> { + self.store.iter().map(|(&k, v)| (k, v.clone())).collect() + } /// Convert borrowed data to Arc pub(crate) fn to_static(&'ctx self) -> Env<'static> { Env { diff --git a/ergotree-interpreter/src/eval/func_value.rs b/ergotree-interpreter/src/eval/func_value.rs index f539db222..c7d1eaef9 100644 --- a/ergotree-interpreter/src/eval/func_value.rs +++ b/ergotree-interpreter/src/eval/func_value.rs @@ -8,10 +8,19 @@ use crate::eval::EvalError; use crate::eval::Evaluable; impl Evaluable for FuncValue { - fn eval<'ctx>(&self, _env: &mut Env, _ctx: &Context<'ctx>) -> Result, EvalError> { + fn eval<'ctx>( + &self, + env: &mut Env<'ctx>, + _ctx: &Context<'ctx>, + ) -> Result, EvalError> { Ok(Value::Lambda(Lambda { args: self.args().to_vec(), body: self.body().clone().into(), + // The JVM's `FuncValue.eval` returns a closure over the defining + // env — capture it so a lambda that escapes its creation site + // (returned from another lambda, bound to a `val`) still sees + // these bindings when applied. + captured: env.bindings(), })) } } diff --git a/ergotree-interpreter/src/eval/scoll.rs b/ergotree-interpreter/src/eval/scoll.rs index 4ac36c6fb..979060d5e 100644 --- a/ergotree-interpreter/src/eval/scoll.rs +++ b/ergotree-interpreter/src/eval/scoll.rs @@ -1,5 +1,5 @@ use crate::eval::EvalError; -use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; use alloc::boxed::Box; use alloc::string::ToString; @@ -49,7 +49,7 @@ pub(crate) static INDEX_OF_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, args| { pub(crate) fn flatmap_eval<'ctx>( _mc: &SMethod, - env: &mut Env<'ctx>, + _env: &mut Env<'ctx>, ctx: &Context<'ctx>, obj: Value<'ctx>, args: Vec>, @@ -80,20 +80,13 @@ pub(crate) fn flatmap_eval<'ctx>( return Err(EvalError::UnexpectedValue(unsupported_msg)); } } - let mut lambda_call = |arg: Value<'ctx>| { - let func_arg = lambda.args.first().ok_or_else(|| { - EvalError::NotFound("flatmap: lambda has empty arguments list".to_string()) - })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = lambda.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res - }; + lambda.args.first().ok_or_else(|| { + EvalError::NotFound("flatmap: lambda has empty arguments list".to_string()) + })?; + // The body evaluates in the lambda's CAPTURED environment (JVM closure + // semantics) — not in the caller's env. + let mut invoker = LambdaInvoker::new(lambda); + let mut lambda_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let mapper_input_tpe = lambda .args .first() diff --git a/ergotree-interpreter/src/eval/soption.rs b/ergotree-interpreter/src/eval/soption.rs index 984b3167a..f11ef5892 100644 --- a/ergotree-interpreter/src/eval/soption.rs +++ b/ergotree-interpreter/src/eval/soption.rs @@ -1,5 +1,5 @@ use crate::eval::EvalError; -use crate::eval::Evaluable; +use crate::eval::LambdaInvoker; use alloc::boxed::Box; use alloc::string::ToString; @@ -12,7 +12,7 @@ use super::Context; pub fn map_eval<'ctx>( _mc: &SMethod, - env: &mut Env<'ctx>, + _env: &mut Env<'ctx>, ctx: &Context<'ctx>, obj: Value<'ctx>, args: Vec>, @@ -30,20 +30,14 @@ pub fn map_eval<'ctx>( input_v_clone ))), }?; - let mut lambda_call = |arg: Value<'ctx>| { - let func_arg = lambda.args.first().ok_or_else(|| { - EvalError::NotFound("map: lambda has empty arguments list".to_string()) - })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = lambda.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res - }; + lambda + .args + .first() + .ok_or_else(|| EvalError::NotFound("map: lambda has empty arguments list".to_string()))?; + // The body evaluates in the lambda's CAPTURED environment (JVM closure + // semantics) — not in the caller's env. + let mut invoker = LambdaInvoker::new(lambda); + let mut lambda_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let normalized_input_val: Option = match input_v { Value::Opt(opt) => Ok(opt.as_deref().cloned()), _ => Err(EvalError::UnexpectedValue(format!( @@ -60,7 +54,7 @@ pub fn map_eval<'ctx>( pub fn filter_eval<'ctx>( _mc: &SMethod, - env: &mut Env<'ctx>, + _env: &mut Env<'ctx>, ctx: &Context<'ctx>, obj: Value<'ctx>, args: Vec>, @@ -78,20 +72,13 @@ pub fn filter_eval<'ctx>( input_v_clone ))), }?; - let mut predicate_call = |arg: Value<'ctx>| { - let func_arg = lambda.args.first().ok_or_else(|| { - EvalError::NotFound("filter: lambda has empty arguments list".to_string()) - })?; - let orig_val = env.get(func_arg.idx).cloned(); - env.insert(func_arg.idx, arg); - let res = lambda.body.eval(env, ctx); - if let Some(orig_val) = orig_val { - env.insert(func_arg.idx, orig_val); - } else { - env.remove(&func_arg.idx); - } - res - }; + lambda.args.first().ok_or_else(|| { + EvalError::NotFound("filter: lambda has empty arguments list".to_string()) + })?; + // The body evaluates in the lambda's CAPTURED environment (JVM closure + // semantics) — not in the caller's env. + let mut invoker = LambdaInvoker::new(lambda); + let mut predicate_call = |arg: Value<'ctx>| invoker.invoke(ctx, vec![arg]); let normalized_input_val: Option = match input_v { Value::Opt(opt) => Ok(opt.as_deref().cloned()), _ => Err(EvalError::UnexpectedValue(format!( diff --git a/ergotree-ir/src/mir/value.rs b/ergotree-ir/src/mir/value.rs index 11f4c2e0d..70425832e 100644 --- a/ergotree-ir/src/mir/value.rs +++ b/ergotree-ir/src/mir/value.rs @@ -28,6 +28,7 @@ use super::constant::TryExtractFromError; use super::constant::TryExtractInto; use super::expr::Expr; use super::func_value::FuncArg; +use super::val_def::ValId; extern crate derive_more; use derive_more::From; @@ -211,11 +212,17 @@ where /// Lambda #[derive(PartialEq, Eq, Debug, Clone)] -pub struct Lambda { +pub struct Lambda<'ctx> { /// Argument placeholders pub args: Vec, /// Body pub body: Box, + /// Bindings captured from the defining environment. The JVM's + /// `FuncValue.eval` returns a closure over the env it was created in, so + /// a lambda that escapes its creation site (returned from another lambda, + /// bound to a `val`) still sees those bindings when applied — the + /// caller's environment at application time plays no role. + pub captured: Vec<(ValId, Value<'ctx>)>, } /// Runtime value @@ -262,7 +269,7 @@ pub enum Value<'ctx> { /// Optional value Opt(Option>>), /// lambda - Lambda(Lambda), + Lambda(Lambda<'ctx>), } impl<'ctx> Value<'ctx> { @@ -304,7 +311,15 @@ impl<'ctx> Value<'ctx> { Value::Global => Value::Global, Value::CBox(c) => Value::CBox(c.to_static()), Value::Opt(opt) => Value::Opt(Option::as_ref(opt).map(|o| o.to_static()).map(Box::new)), - Value::Lambda(l) => Value::Lambda(l.clone()), + Value::Lambda(l) => Value::Lambda(Lambda { + args: l.args.clone(), + body: l.body.clone(), + captured: l + .captured + .iter() + .map(|(k, v)| (*k, v.to_static())) + .collect(), + }), } } }