From 79df1cae89db345104124c209fc320a4afcb5878 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:19:53 +0000 Subject: [PATCH 1/2] Fix `NameError` for forward function references by adding a pre-execution hoisting pass This commit fixes a bug in `eldritch-core` where functions failed to resolve references to other functions that were defined later in the same execution block. By introducing a `hoist_functions` pre-pass, function signatures are successfully evaluated and stored in the environment before any statements are executed, permitting forward-references. Default arguments are skipped during hoisting and correctly evaluated during the sequential evaluation pass. This maintains compatibility with dynamically evaluated parameters that rely on prior sequentially-executed statements in the same scope, and accurately resolves redefinitions. Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com> --- .../eldritch-core/src/interpreter/core.rs | 4 ++ .../eldritch-core/src/interpreter/exec.rs | 40 +++++++++++++++ .../tests/regression_forward_ref.rs | 50 +++++++++++++++++++ .../eldritch/eldritch-core/tests/test_eval.rs | 18 +++++++ .../eldritch-core/tests/test_eval_2.rs | 19 +++++++ .../eldritch-core/tests/test_eval_split.rs | 14 ++++++ .../eldritch-core/tests/test_hoist.rs | 19 +++++++ .../eldritch-core/tests/test_nested.rs | 21 ++++++++ patch.py | 32 ++++++++++++ 9 files changed, 217 insertions(+) create mode 100644 implants/lib/eldritch/eldritch-core/tests/regression_forward_ref.rs create mode 100644 implants/lib/eldritch/eldritch-core/tests/test_eval.rs create mode 100644 implants/lib/eldritch/eldritch-core/tests/test_eval_2.rs create mode 100644 implants/lib/eldritch/eldritch-core/tests/test_eval_split.rs create mode 100644 implants/lib/eldritch/eldritch-core/tests/test_hoist.rs create mode 100644 implants/lib/eldritch/eldritch-core/tests/test_nested.rs create mode 100644 patch.py diff --git a/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs b/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs index 3e20729e3..a1d77719b 100644 --- a/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs +++ b/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs @@ -202,6 +202,10 @@ impl Interpreter { self.call_stack.clear(); self.current_func_name = "".to_string(); + if let Err(e) = exec::hoist_functions(self, &stmts) { + return Err(self.format_error(input, e)); + } + for stmt in stmts { match &stmt.kind { // Special case: if top-level statement is an expression, return its value diff --git a/implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs b/implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs index f3271246b..3a1fe7378 100644 --- a/implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs +++ b/implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs @@ -155,6 +155,46 @@ pub fn execute(interp: &mut Interpreter, stmt: &Stmt) -> Result<(), EldritchErro Ok(()) } +pub fn hoist_functions(interp: &mut Interpreter, stmts: &[Stmt]) -> Result<(), EldritchError> { + // Collect functions to hoist so we don't hold read locks while evaluating default params + // Only hoist the *first* definition of a given name in this block, to allow forward references, + // while sequential execution will properly overwrite later definitions. + let mut to_hoist = Vec::new(); + let mut seen = BTreeSet::new(); + + for stmt in stmts { + if let StmtKind::Def(name, params, _return_annotation, body) = &stmt.kind { + if !seen.contains(name) { + seen.insert(name.clone()); + to_hoist.push((name, params, body)); + } + } + } + + for (name, params, body) in to_hoist { + let mut runtime_params = Vec::new(); + for param in params { + match param { + Param::Normal(n, _) => runtime_params.push(RuntimeParam::Normal(n.clone())), + Param::Star(n, _) => runtime_params.push(RuntimeParam::Star(n.clone())), + Param::StarStar(n, _) => runtime_params.push(RuntimeParam::StarStar(n.clone())), + Param::WithDefault(n, _, _) => { + runtime_params.push(RuntimeParam::WithDefault(n.clone(), Value::None)); + } + } + } + + let func = Value::Function(Function { + name: name.clone(), + params: runtime_params, + body: body.clone(), + closure: interp.env.clone(), + }); + interp.env.write().values.insert(name.clone(), func); + } + Ok(()) +} + pub fn execute_stmts(interp: &mut Interpreter, stmts: &[Stmt]) -> Result<(), EldritchError> { for stmt in stmts { execute(interp, stmt)?; diff --git a/implants/lib/eldritch/eldritch-core/tests/regression_forward_ref.rs b/implants/lib/eldritch/eldritch-core/tests/regression_forward_ref.rs new file mode 100644 index 000000000..17b7a1b0b --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/regression_forward_ref.rs @@ -0,0 +1,50 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_forward_reference() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + r#" +def b(): + a() + +def a(): + print("A") + +b() +"#, + ); + if let Err(e) = res { + panic!("Failed with: {}", e); + } +} + +#[test] +fn test_redefine() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + r#" +def b(): + return a() + +def a(): + return "A" + +res1 = b() + +def a(): + return "A2" + +res2 = b() +"#, + ); + if let Err(e) = res { + panic!("Failed with: {}", e); + } + // Verify values + let env = interp.env.read(); + let res1 = env.values.get("res1").unwrap(); + let res2 = env.values.get("res2").unwrap(); + assert_eq!(res1, &eldritch_core::Value::String("A".to_string())); + assert_eq!(res2, &eldritch_core::Value::String("A2".to_string())); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/test_eval.rs b/implants/lib/eldritch/eldritch-core/tests/test_eval.rs new file mode 100644 index 000000000..27a215548 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/test_eval.rs @@ -0,0 +1,18 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_manual() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + " +def b(): + a() + +def a(): + print(\"A\") + +b() +", + ); + println!("result is {:?}", res); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/test_eval_2.rs b/implants/lib/eldritch/eldritch-core/tests/test_eval_2.rs new file mode 100644 index 000000000..845e841df --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/test_eval_2.rs @@ -0,0 +1,19 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_manual() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + " +def b(): + a() + +def a(): + print(\"A\") + +b() +", + ); + println!("result is {:?}", res); + assert!(res.is_ok()); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/test_eval_split.rs b/implants/lib/eldritch/eldritch-core/tests/test_eval_split.rs new file mode 100644 index 000000000..73d10f0d2 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/test_eval_split.rs @@ -0,0 +1,14 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_manual() { + let mut interp = Interpreter::new(); + let res = interp.interpret("def b():\n a()"); + println!("def b: {:?}", res); + + let res = interp.interpret("def a():\n print(\"A\")"); + println!("def a: {:?}", res); + + let res = interp.interpret("b()"); + println!("result is {:?}", res); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/test_hoist.rs b/implants/lib/eldritch/eldritch-core/tests/test_hoist.rs new file mode 100644 index 000000000..34d268ea3 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/test_hoist.rs @@ -0,0 +1,19 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_hoist() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + " +def b(): + a() + +b() + +def a(): + print(\"A\") +", + ); + println!("result is {:?}", res); + assert!(res.is_ok()); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/test_nested.rs b/implants/lib/eldritch/eldritch-core/tests/test_nested.rs new file mode 100644 index 000000000..dab6f11f5 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/test_nested.rs @@ -0,0 +1,21 @@ +use eldritch_core::Interpreter; + +#[test] +fn test_nested() { + let mut interp = Interpreter::new(); + let res = interp.interpret( + " +def c(): + def b(): + a() + def a(): + print(\"A\") + b() + +c() +", + ); + if let Err(e) = res { + panic!("Failed with: {}", e); + } +} diff --git a/patch.py b/patch.py new file mode 100644 index 000000000..6999b721d --- /dev/null +++ b/patch.py @@ -0,0 +1,32 @@ +with open("./implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs", "r") as f: + code = f.read() + +# Restore the main `execute` function def logic +bad_def = """ Param::WithDefault(n, _, _) => { + // Do not evaluate default expressions during hoisting to prevent + // NameErrors on variables defined earlier in the same block. + // The actual sequential execution will correctly evaluate them. + runtime_params.push(RuntimeParam::WithDefault(n.clone(), Value::None)); + }""" + +good_def = """ Param::WithDefault(n, _type, default_expr) => { + let val = evaluate(interp, default_expr)?; + runtime_params.push(RuntimeParam::WithDefault(n.clone(), val)); + }""" + +code = code.replace(bad_def, good_def) + +# Apply Value::None logic to the `hoist_functions` method instead +hoist_bad = """ Param::WithDefault(n, _, default_expr) => { + let val = evaluate(interp, default_expr)?; + runtime_params.push(RuntimeParam::WithDefault(n.clone(), val)); + }""" + +hoist_good = """ Param::WithDefault(n, _, _) => { + runtime_params.push(RuntimeParam::WithDefault(n.clone(), Value::None)); + }""" + +code = code.replace(hoist_bad, hoist_good) + +with open("./implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs", "w") as f: + f.write(code) From e022c5944b53809d0432cedf2dfaa7ef775b2e0d Mon Sep 17 00:00:00 2001 From: KCarretto Date: Sat, 14 Mar 2026 19:34:40 +0000 Subject: [PATCH 2/2] build --- patch.py | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 patch.py diff --git a/patch.py b/patch.py deleted file mode 100644 index 6999b721d..000000000 --- a/patch.py +++ /dev/null @@ -1,32 +0,0 @@ -with open("./implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs", "r") as f: - code = f.read() - -# Restore the main `execute` function def logic -bad_def = """ Param::WithDefault(n, _, _) => { - // Do not evaluate default expressions during hoisting to prevent - // NameErrors on variables defined earlier in the same block. - // The actual sequential execution will correctly evaluate them. - runtime_params.push(RuntimeParam::WithDefault(n.clone(), Value::None)); - }""" - -good_def = """ Param::WithDefault(n, _type, default_expr) => { - let val = evaluate(interp, default_expr)?; - runtime_params.push(RuntimeParam::WithDefault(n.clone(), val)); - }""" - -code = code.replace(bad_def, good_def) - -# Apply Value::None logic to the `hoist_functions` method instead -hoist_bad = """ Param::WithDefault(n, _, default_expr) => { - let val = evaluate(interp, default_expr)?; - runtime_params.push(RuntimeParam::WithDefault(n.clone(), val)); - }""" - -hoist_good = """ Param::WithDefault(n, _, _) => { - runtime_params.push(RuntimeParam::WithDefault(n.clone(), Value::None)); - }""" - -code = code.replace(hoist_bad, hoist_good) - -with open("./implants/lib/eldritch/eldritch-core/src/interpreter/exec.rs", "w") as f: - f.write(code)