From 3979c87e527c6b95988a1a053e1c3bff92d318a8 Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:10:30 +0800 Subject: [PATCH 1/6] feat(M5c): add D3120/D3121 error templates for $eval --- spec/eval_spec.lua | 16 ++++++++++++++++ src/jsonata/errors.lua | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 spec/eval_spec.lua diff --git a/spec/eval_spec.lua b/spec/eval_spec.lua new file mode 100644 index 0000000..4c0df3d --- /dev/null +++ b/spec/eval_spec.lua @@ -0,0 +1,16 @@ +local errors = require("jsonata.errors") + +describe("M5c: D3120/D3121 error templates", function() + it("defines D3120 and D3121 with interpolatable {{value}}", function() + local ok1, e1 = pcall(errors.raise, "D3120", { value = "boom" }) + assert.is_false(ok1) + assert.are.equal("D3120", e1.code) + assert.is_not_nil(e1.message:find("eval", 1, true)) + assert.is_nil(e1.message:find("{{", 1, true)) -- {{value}} interpolated + + local ok2, e2 = pcall(errors.raise, "D3121", { value = "boom" }) + assert.is_false(ok2) + assert.are.equal("D3121", e2.code) + assert.is_nil(e2.message:find("{{", 1, true)) + end) +end) diff --git a/src/jsonata/errors.lua b/src/jsonata/errors.lua index 6be945e..e51ded1 100644 --- a/src/jsonata/errors.lua +++ b/src/jsonata/errors.lua @@ -42,6 +42,8 @@ local MESSAGES = { D3138 = "The single() function expected exactly 1 matching result. Instead it matched more.", D3139 = "The single() function expected exactly 1 matching result. Instead it matched 0.", D3141 = "$assert() statement failed", + D3120 = "Syntax error in expression passed to function eval: {{value}}", + D3121 = "Dynamic error evaluating the expression passed to function eval: {{value}}", } function M.is_error(x) From c349d50e8b4868cb5cb1e211667b959c38dc5bbf Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:12:41 +0800 Subject: [PATCH 2/6] feat(M5c): wants_env hook in M.apply (env+input for flagged builtins; dormant) --- spec/eval_spec.lua | 37 +++++++++++++++++++++++++++++++++++++ src/jsonata/evaluator.lua | 7 +++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/spec/eval_spec.lua b/spec/eval_spec.lua index 4c0df3d..77f04e7 100644 --- a/spec/eval_spec.lua +++ b/spec/eval_spec.lua @@ -14,3 +14,40 @@ describe("M5c: D3120/D3121 error templates", function() assert.is_nil(e2.message:find("{{", 1, true)) end) end) + +describe("M5c: M.apply wants_env hook", function() + local E = require("jsonata.evaluator") + local Environment = require("jsonata.environment") + + it("passes (env, input, ...args) to a wants_env builtin", function() + local seen = {} + local proc = { + _jsonata_function = true, + wants_env = true, + impl = function(env, input, a) + seen.env, seen.input, seen.a = env, input, a + return a + end, + } + local env = Environment.new() + local r = E.apply(proc, { 42 }, "INPUT", env) + assert.are.equal(42, r) + assert.are.equal("INPUT", seen.input) + assert.are.equal(env, seen.env) + assert.are.equal(42, seen.a) + end) + + it("a normal builtin gets only its args (no env/input prefix)", function() + local seen = {} + local proc = { + _jsonata_function = true, + impl = function(a, b) + seen.a, seen.b = a, b + return a + end, + } + E.apply(proc, { 7, 8 }, "INPUT", Environment.new()) + assert.are.equal(7, seen.a) + assert.are.equal(8, seen.b) -- NOT shifted by an env/input prefix + end) +end) diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index 8e76b4d..352befd 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -894,7 +894,7 @@ evaluate = function(node, input, env) end -- Apply a procedure (lambda closure or builtin) to a list of evaluated args. -function M.apply(proc, args, context) +function M.apply(proc, args, context, env) if type(proc) == "table" and proc._jsonata_lambda then return M.apply_lambda(proc, args, context) end @@ -902,6 +902,9 @@ function M.apply(proc, args, context) if proc.signature then args = proc.signature.validate(args, context) end + if proc.wants_env then + return proc.impl(env, context, (table.unpack or unpack)(args, 1, #args)) + end return proc.impl((table.unpack or unpack)(args, 1, #args)) end errors.raise("T1006", { value = proc }) @@ -933,7 +936,7 @@ function M.eval_function(node, input, env) for i, a in ipairs(node.arguments) do args[i] = evaluate(a, input, env) end - local result = M.apply(proc, args, input) + local result = M.apply(proc, args, input, env) if V.is_sequence(result) then return finalize_sequence(result, false) end From b8eefe019e50eb21bcee83e46bfa9ad73dab96c0 Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:17:52 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(M5c):=20$eval=20builtin=20=E2=80=94=20?= =?UTF-8?q?parse+evaluate=20via=20the=20wants=5Fenv=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/eval_spec.lua | 27 ++++++++++++++++ src/jsonata/functions/eval.lua | 56 ++++++++++++++++++++++++++++++++++ src/jsonata/functions/init.lua | 1 + 3 files changed, 84 insertions(+) create mode 100644 src/jsonata/functions/eval.lua diff --git a/spec/eval_spec.lua b/spec/eval_spec.lua index 77f04e7..a0bf447 100644 --- a/spec/eval_spec.lua +++ b/spec/eval_spec.lua @@ -51,3 +51,30 @@ describe("M5c: M.apply wants_env hook", function() assert.are.equal(8, seen.b) -- NOT shifted by an env/input prefix end) end) + +describe("M5c: $eval", function() + local jsonata = require("jsonata") + local function run(src, input) + return jsonata.compile(src):evaluate(input) + end + + it("parses and evaluates a literal expression", function() + assert.are.same({ 1, 2, 3 }, run("$eval('[1,2,3]')")) + end) + + it("sees builtins in the evaluated string", function() + assert.are.same({ 1, "2", 3 }, run("$eval('[1,$string(2),3]')")) + end) + + it("returns undefined for an undefined argument", function() + assert.is_nil(run("$eval(nope)", {})) + end) + + it("evaluates against the current input by default", function() + assert.are.equal(6, run("$eval('a + b + c')", { a = 1, b = 2, c = 3 })) + end) + + it("uses an explicit 2nd-arg context override", function() + assert.are.equal(6, run("$eval('x*y*z', sub)", { sub = { x = 1, y = 2, z = 3 } })) + end) +end) diff --git a/src/jsonata/functions/eval.lua b/src/jsonata/functions/eval.lua new file mode 100644 index 0000000..84f6162 --- /dev/null +++ b/src/jsonata/functions/eval.lua @@ -0,0 +1,56 @@ +local V = require("jsonata.value") +local errors = require("jsonata.errors") + +local R = {} + +-- Lazy requires (break the evaluator -> functions -> evaluator load cycle; memoized). +local parser, evaluator +local function get_parser() + parser = parser or require("jsonata.parser") + return parser +end +local function get_evaluator() + evaluator = evaluator or require("jsonata.evaluator") + return evaluator +end + +-- $eval(expr [, focus]) — parse `expr` as JSONata and evaluate it against the +-- current environment + the call-site input (or `focus` if supplied). +-- wants_env: M.apply prepends (env, input) ahead of the validated args. +local function eval_impl(env, input, expr, focus) + if V.is_nothing(expr) then + return V.NOTHING + end + + -- Parse — non-recovering; any syntax error becomes D3120. + local ok, ast = pcall(get_parser().parse, expr) + if not ok then + errors.raise("D3120", { value = (type(ast) == "table" and ast.message) or tostring(ast) }) + end + + -- Pick the input: explicit focus overrides; else the threaded call-site input. + local target = focus + if V.is_nothing(focus) then + target = input + end + + -- Evaluate against the CURRENT env — any runtime error becomes D3121. + local ok2, result = pcall(get_evaluator().evaluate, ast, target, env) + if not ok2 then + errors.raise("D3121", { value = (type(result) == "table" and result.message) or tostring(result) }) + end + return result +end + +-- Build the def manually: with wants_env, M.apply calls proc.impl(env, input, ...validated_args). +-- The H.def arity-checking wrapper would count env+input as extra args and fail. +-- The signature already validates the user-supplied args; we just need arity=1 for HOF hints. +R.eval = { + _jsonata_function = true, + impl = eval_impl, + arity = 1, + signature = require("jsonata.signature").parse(""), + wants_env = true, +} + +return R diff --git a/src/jsonata/functions/init.lua b/src/jsonata/functions/init.lua index 7c6427d..9f97839 100644 --- a/src/jsonata/functions/init.lua +++ b/src/jsonata/functions/init.lua @@ -11,6 +11,7 @@ local categories = { require("jsonata.functions.array"), require("jsonata.functions.object"), require("jsonata.functions.higher_order"), + require("jsonata.functions.eval"), } M.registry = {} From 3e68b522dcf1973a7cd4c6a6123a7606c772ef3a Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:28:03 +0800 Subject: [PATCH 4/6] test(M5c): \$eval outer-scope visibility + D3120/D3121 boundary Co-Authored-By: Claude Sonnet 4.6 --- spec/eval_spec.lua | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/eval_spec.lua b/spec/eval_spec.lua index a0bf447..2aaca7c 100644 --- a/spec/eval_spec.lua +++ b/spec/eval_spec.lua @@ -78,3 +78,31 @@ describe("M5c: $eval", function() assert.are.equal(6, run("$eval('x*y*z', sub)", { sub = { x = 1, y = 2, z = 3 } })) end) end) + +describe("M5c: $eval outer-scope + error boundary", function() + local jsonata = require("jsonata") + local function run(src, input) + return jsonata.compile(src):evaluate(input) + end + local function code(src, input) + local ok, err = pcall(run, src, input) + assert.is_false(ok) + return err.code + end + + it("sees an outer-scope bound variable (option i)", function() + assert.are.equal(5, run("($x := 5; $eval('$x'))")) + end) + + it("raises D3120 on a syntax error in the expression", function() + assert.are.equal("D3120", code("$eval('[1,#string(2),3]')")) + end) + + it("raises D3121 on a runtime error in the expression", function() + assert.are.equal("D3121", code("$eval('[1,string(2),3]')")) + end) + + it("can recurse", function() + assert.are.equal(2, run("$eval('$eval(\"1+1\")')")) + end) +end) From 489757f3d49914facf27efd752046da47a175814 Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:30:11 +0800 Subject: [PATCH 5/6] test(M5c): regenerate official-suite baseline ($eval / function-eval group) Co-Authored-By: Claude Sonnet 4.6 --- spec/jsonata-suite/baseline.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/jsonata-suite/baseline.lua b/spec/jsonata-suite/baseline.lua index 7266d14..d99abbd 100644 --- a/spec/jsonata-suite/baseline.lua +++ b/spec/jsonata-suite/baseline.lua @@ -343,8 +343,18 @@ return { ["function-error/case008"] = true, ["function-error/case009"] = true, ["function-error/case010"] = true, + ["function-eval/case000"] = true, + ["function-eval/case001"] = true, + ["function-eval/case002"] = true, + ["function-eval/case003"] = true, + ["function-eval/case004"] = true, + ["function-eval/case005"] = true, ["function-eval/case006"] = true, ["function-eval/case007"] = true, + ["function-eval/case008/0"] = true, + ["function-eval/case008/1"] = true, + ["function-eval/case008/2"] = true, + ["function-eval/case008/3"] = true, ["function-exists/case000"] = true, ["function-exists/case001"] = true, ["function-exists/case002"] = true, From 465c60518690867a254da6b05435b7d768d3f0cf Mon Sep 17 00:00:00 2001 From: fl Date: Tue, 23 Jun 2026 23:47:34 +0800 Subject: [PATCH 6/6] fix(M5c): thread env into ~>/partial apply ($eval via ~>); strip cons on array focus --- spec/eval_spec.lua | 29 +++++++++++++++++++++++++++++ src/jsonata/evaluator.lua | 6 +++--- src/jsonata/functions/eval.lua | 9 +++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/spec/eval_spec.lua b/spec/eval_spec.lua index 2aaca7c..fb23277 100644 --- a/spec/eval_spec.lua +++ b/spec/eval_spec.lua @@ -106,3 +106,32 @@ describe("M5c: $eval outer-scope + error boundary", function() assert.are.equal(2, run("$eval('$eval(\"1+1\")')")) end) end) + +describe("M5c: $eval via ~> and array focus (adversarial fixes)", function() + local jsonata = require("jsonata") + local function run(src, input) + return jsonata.compile(src):evaluate(input) + end + + it("works when applied via ~> (env threaded, no crash)", function() + assert.are.equal(2, run("'1+1' ~> $eval")) + end) + + it("sees outer-scope vars when applied via ~>", function() + assert.are.equal(8, run("($x := 7; '$x+1' ~> $eval)")) + end) + + it("indexes a non-sequence array focus correctly", function() + assert.are.equal(10, run("$eval('$[0]', [10,20,30])")) + assert.are.equal(20, run("$eval('$[1]', [10,20,30])")) + end) + + it("maps over a non-sequence array focus correctly", function() + assert.are.same({ 1, 2 }, run("$eval('$.a', [{'a':1},{'a':2}])")) + end) + + it("still treats an input-independent expr the same (case008 intact)", function() + local r = run("$eval('{\"test\": 1}', [1,2,3])") + assert.are.equal(1, r.test) + end) +end) diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index 352befd..6a19155 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -781,9 +781,9 @@ local function _evaluate(node, input, env) for _, a in ipairs(rhs.arguments) do args[#args + 1] = evaluate(a, input, env) end - return M.apply(proc, args, input) + return M.apply(proc, args, input, env) end - return M.apply(evaluate(rhs, input, env), { lhs }, input) + return M.apply(evaluate(rhs, input, env), { lhs }, input, env) elseif t == "transform" then return { _jsonata_function = true, @@ -968,7 +968,7 @@ function M.partial(proc, argnodes, input, env) for k, pos in ipairs(holes) do args[pos] = fill[k] end - return M.apply(proc, args) + return M.apply(proc, args, input, env) end, } end diff --git a/src/jsonata/functions/eval.lua b/src/jsonata/functions/eval.lua index 84f6162..be81577 100644 --- a/src/jsonata/functions/eval.lua +++ b/src/jsonata/functions/eval.lua @@ -29,9 +29,18 @@ local function eval_impl(env, input, expr, focus) end -- Pick the input: explicit focus overrides; else the threaded call-site input. + -- A non-sequence array focus carries the internal `cons` flag (from array + -- construction), which corrupts path steps; rebuild it clean (jsonata wraps a + -- non-sequence array focus so it's treated as a normal value). local target = focus if V.is_nothing(focus) then target = input + elseif V.is_array(focus) and not V.is_sequence(focus) then + local clean = {} + for i = 1, #focus do + clean[i] = focus[i] + end + target = V.array(clean) end -- Evaluate against the CURRENT env — any runtime error becomes D3121.