From a8c3c77021e145c43525469500a527a6766876cc Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:17:08 +0800 Subject: [PATCH 1/6] feat(M6a): bind input under "$" so $$ (root context) resolves --- spec/m6a_spec.lua | 20 ++++++++++++++++++++ src/jsonata/init.lua | 1 + 2 files changed, 21 insertions(+) create mode 100644 spec/m6a_spec.lua diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua new file mode 100644 index 0000000..bed8f1a --- /dev/null +++ b/spec/m6a_spec.lua @@ -0,0 +1,20 @@ +local jsonata = require("jsonata") +local function run(src, input) + return jsonata.compile(src):evaluate(input) +end + +describe("M6a C-1: $$ root context", function() + it("$$ resolves to the root input", function() + assert.are.same({ a = 1 }, run("$$", { a = 1 })) + end) + it("$$ reaches root from a nested path step", function() + assert.are.same({ a = { b = 2 } }, run("a.$$", { a = { b = 2 } })) + end) + it("$$.field indexes the root", function() + assert.are.equal(1, run("$$.a", { a = 1 })) + end) + it("$ (current context) still works", function() + assert.are.equal(5, run("a.$", { a = 5 })) + assert.are.same({ x = 1 }, run("$", { x = 1 })) + end) +end) diff --git a/src/jsonata/init.lua b/src/jsonata/init.lua index 27aed2a..1c646db 100644 --- a/src/jsonata/init.lua +++ b/src/jsonata/init.lua @@ -69,6 +69,7 @@ function Expression:evaluate(input, bindings) env:bind("__explain_hook", self._explain_hook) end local internal_input = adapter.from_lua(input) + env:bind("$", internal_input) local result = Evaluator.evaluate(self.ast, internal_input, env) return adapter.to_lua(result) end From 0d34ff6567e97a3c1ba20547657c9663ac34db92 Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:17:59 +0800 Subject: [PATCH 2/6] feat(M6a): $keys empty->undefined; $spread echoes scalars --- spec/m6a_spec.lua | 17 +++++++++++++++++ src/jsonata/functions/object.lua | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua index bed8f1a..e8f619e 100644 --- a/spec/m6a_spec.lua +++ b/spec/m6a_spec.lua @@ -18,3 +18,20 @@ describe("M6a C-1: $$ root context", function() assert.are.same({ x = 1 }, run("$", { x = 1 })) end) end) + +describe("M6a B-3: $keys/$spread empty & scalar", function() + it("$keys of an empty result is undefined, not []", function() + assert.is_nil(run("$keys(5)")) + assert.is_nil(run("$keys({})")) + end) + it("$keys still returns the keys of an object", function() + assert.are.same({ "a", "b" }, run('$keys({"a":1, "b":2})')) + end) + it("$spread echoes a scalar argument", function() + assert.are.equal(5, run("$spread(5)")) + assert.is_true(run("$spread(true)")) + end) + it("$spread still spreads an object", function() + assert.are.same({ { a = 1 } }, run('$spread({"a":1})')) + end) +end) diff --git a/src/jsonata/functions/object.lua b/src/jsonata/functions/object.lua index 85edee4..f3b8082 100644 --- a/src/jsonata/functions/object.lua +++ b/src/jsonata/functions/object.lua @@ -25,6 +25,9 @@ R.keys = H.def(function(x) end end end + if #out == 0 then + return V.NOTHING + end return out end, 1, 1, ">") @@ -75,6 +78,8 @@ R.spread = H.def(function(x) spread_obj(x[i]) end end + else + return x -- scalar: jsonata functionSpread echoes the argument unchanged end return out end, 1, 1, ">") From 60030b63cf8bc9978317ed33e0a81117d7561268 Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:19:48 +0800 Subject: [PATCH 3/6] feat(M6a): undefined-operand arithmetic returns undefined (binary + unary) --- spec/m6a_spec.lua | 30 ++++++++++++++++++++++++++++++ src/jsonata/evaluator.lua | 19 ++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua index e8f619e..2732df6 100644 --- a/spec/m6a_spec.lua +++ b/spec/m6a_spec.lua @@ -35,3 +35,33 @@ describe("M6a B-3: $keys/$spread empty & scalar", function() assert.are.same({ { a = 1 } }, run('$spread({"a":1})')) end) end) + +describe("M6a C-4a: undefined-operand arithmetic", function() + local function code(src, input) + local ok, err = pcall(run, src, input) + assert.is_false(ok) + return err.code + end + it("arithmetic with an undefined operand is undefined", function() + assert.is_nil(run("5 + nope", {})) + assert.is_nil(run("nope - 1", {})) + assert.is_nil(run("nope * 2", {})) + assert.is_nil(run("10 / nope", {})) + assert.is_nil(run("-nope", {})) + end) + it("normal arithmetic still works", function() + assert.are.equal(7, run("5 + 2")) + assert.are.equal(-3, run("-(1 + 2)")) + end) + it("a genuine non-number operand still raises a type error", function() + assert.are.equal("T2001", code("'x' + 5")) + assert.are.equal("T2002", code("5 + 'x'")) + end) + it("matches jsonata operand ordering (type error before undefined short-circuit)", function() + assert.are.equal("T2002", code("nope + 'x'", {})) -- undefined LHS, defined non-number RHS -> T2002 + assert.are.equal("T2001", code("'x' + nope", {})) -- defined non-number LHS -> T2001 + assert.is_nil(run("nope + 5", {})) -- undefined LHS, number RHS -> undefined + assert.is_nil(run("5 + nope", {})) -- number LHS, undefined RHS -> undefined + assert.are.equal("T2001", code("false + nope", {})) -- defined non-number LHS (case018) -> T2001 + end) +end) diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index 0aabf96..bc9535e 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -59,8 +59,17 @@ local function eval_binary(node, input, env) end if op == "+" or op == "-" or op == "*" or op == "/" or op == "%" then - local a = as_number(lhs, "T2001") - local b = as_number(rhs, "T2002") + if not V.is_nothing(lhs) and V.typeof(lhs) ~= "number" then + errors.raise("T2001", { value = lhs }) + end + if not V.is_nothing(rhs) and V.typeof(rhs) ~= "number" then + errors.raise("T2002", { value = rhs }) + end + if V.is_nothing(lhs) or V.is_nothing(rhs) then + return V.NOTHING + end + local a = lhs + local b = rhs if op == "+" then return a + b elseif op == "-" then @@ -696,7 +705,11 @@ local function _evaluate(node, input, env) return V.NULL elseif t == "unary" then if node.value == "-" then - return -as_number(evaluate(node.expression, input, env), "T2001") + local v = evaluate(node.expression, input, env) + if V.is_nothing(v) then + return V.NOTHING + end + return -as_number(v, "T2001") end errors.raise("S0211", { token = node.value }) elseif t == "binary" then From f551ae7922048b68acb980f79d34ae73974e086a Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:20:45 +0800 Subject: [PATCH 4/6] feat(M6a): object-literal key rules (undefined skip/omit, T1003, D1009) --- spec/m6a_spec.lua | 23 +++++++++++++++++++++++ src/jsonata/evaluator.lua | 15 ++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua index 2732df6..05eef0e 100644 --- a/spec/m6a_spec.lua +++ b/spec/m6a_spec.lua @@ -65,3 +65,26 @@ describe("M6a C-4a: undefined-operand arithmetic", function() assert.are.equal("T2001", code("false + nope", {})) -- defined non-number LHS (case018) -> T2001 end) end) + +describe("M6a C-3: object-literal key rules", function() + local function code(src, input) + local ok, err = pcall(run, src, input) + assert.is_false(ok) + return err.code + end + it("an undefined key skips the whole pair", function() + assert.are.same({}, run("{nope: 1}", {})) + end) + it("an undefined value omits the pair", function() + assert.are.same({}, run('{"a": nope}', {})) + end) + it("a non-string key raises T1003", function() + assert.are.equal("T1003", code('{1: "x"}')) + end) + it("a duplicate key raises D1009", function() + assert.are.equal("D1009", code('{"a":1, "a":2}')) + end) + it("a normal object literal still builds correctly", function() + assert.are.equal(2, run('{"a":1, "b":2}.b', {})) + end) +end) diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index bc9535e..31e50e5 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -780,9 +780,18 @@ local function _evaluate(node, input, env) local obj = V.object() for _, pair in ipairs(node.pairs) do local k = evaluate(pair[1], input, env) - local val = evaluate(pair[2], input, env) - local kstr = V.is_nothing(k) and "" or functions.string.impl(k) - V.obj_set(obj, kstr, val) + if not V.is_nothing(k) then -- undefined key: skip the whole pair + if V.typeof(k) ~= "string" then + errors.raise("T1003", { value = k }) + end + local val = evaluate(pair[2], input, env) + if not V.is_nothing(val) then -- undefined value: omit the pair + if not V.is_nothing(V.obj_get(obj, k)) then + errors.raise("D1009", { value = k }) + end + V.obj_set(obj, k, val) + end + end end return obj elseif t == "function" then From 204ab63e97f2615e5463107152a692d899327467 Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:38:29 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(M6a):=20adversarial=20fidelity=20?= =?UTF-8?q?=E2=80=94=20\$spread=20empty->undefined,=20dup-key=20D1009=20(a?= =?UTF-8?q?ny=20value),=20unary=20minus=20D1002?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/m6a_spec.lua | 26 ++++++++++++++++++++++++++ src/jsonata/errors.lua | 1 + src/jsonata/evaluator.lua | 13 +++++++++---- src/jsonata/functions/object.lua | 3 +++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua index 05eef0e..fa3ab9f 100644 --- a/spec/m6a_spec.lua +++ b/spec/m6a_spec.lua @@ -88,3 +88,29 @@ describe("M6a C-3: object-literal key rules", function() assert.are.equal(2, run('{"a":1, "b":2}.b', {})) end) end) + +describe("M6a adversarial fixes", function() + local function code(src, input) + local ok, err = pcall(run, src, input) + assert.is_false(ok) + return err.code + end + + it("$spread of an empty object/array is undefined", function() + assert.is_nil(run("$spread({})")) + assert.is_nil(run("$spread([])")) + end) + + it("a duplicate object key with an undefined value still raises D1009", function() + assert.are.equal("D1009", code('{"a":1, "a":nope}', {})) + assert.are.equal("D1009", code('{"a":nope, "a":2}', {})) + assert.are.equal("D1009", code('{"a":nope, "a":nope}', {})) + end) + + it("unary minus on a non-number raises D1002", function() + assert.are.equal("D1002", code("-'x'")) + assert.are.equal("D1002", code("-true")) + assert.is_nil(run("-nope", {})) -- undefined still propagates + assert.are.equal(-5, run("-5")) -- normal still works + end) +end) diff --git a/src/jsonata/errors.lua b/src/jsonata/errors.lua index 27aecdc..25731db 100644 --- a/src/jsonata/errors.lua +++ b/src/jsonata/errors.lua @@ -29,6 +29,7 @@ local MESSAGES = { T2012 = "The delete clause of the transform expression must evaluate to an array of strings", -- Dynamic / runtime errors D1001 = "Number out of range to be formatted", + D1002 = "Cannot negate a non-numeric value: {{value}}", D1009 = "Multiple key definitions evaluate to same key: {{value}}", D2014 = "The size of the sequence allocated by the range operator (..) must not exceed 1e7. Attempted to allocate {{value}}.", D3001 = "Unsupported in M1", diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index 31e50e5..3eb81e6 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -709,7 +709,10 @@ local function _evaluate(node, input, env) if V.is_nothing(v) then return V.NOTHING end - return -as_number(v, "T2001") + if V.typeof(v) ~= "number" then + errors.raise("D1002", { value = v }) + end + return -v end errors.raise("S0211", { token = node.value }) elseif t == "binary" then @@ -778,17 +781,19 @@ local function _evaluate(node, input, env) return arr elseif t == "object" then local obj = V.object() + local seen = {} for _, pair in ipairs(node.pairs) do local k = evaluate(pair[1], input, env) if not V.is_nothing(k) then -- undefined key: skip the whole pair if V.typeof(k) ~= "string" then errors.raise("T1003", { value = k }) end + if seen[k] then -- duplicate key (regardless of value) + errors.raise("D1009", { value = k }) + end + seen[k] = true local val = evaluate(pair[2], input, env) if not V.is_nothing(val) then -- undefined value: omit the pair - if not V.is_nothing(V.obj_get(obj, k)) then - errors.raise("D1009", { value = k }) - end V.obj_set(obj, k, val) end end diff --git a/src/jsonata/functions/object.lua b/src/jsonata/functions/object.lua index f3b8082..f8f5f26 100644 --- a/src/jsonata/functions/object.lua +++ b/src/jsonata/functions/object.lua @@ -81,6 +81,9 @@ R.spread = H.def(function(x) else return x -- scalar: jsonata functionSpread echoes the argument unchanged end + if #out == 0 then + return V.NOTHING + end return out end, 1, 1, ">") From c0a3b2ad03c07bb148624bd62c025c7861494bcb Mon Sep 17 00:00:00 2001 From: fl Date: Wed, 24 Jun 2026 11:39:59 +0800 Subject: [PATCH 6/6] test(M6a): regenerate official-suite baseline (semantic-cluster gains) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1153 → 1174 (+21) out of 1682 total. Newly-passing groups: conditionals +4 (case000/001/003/004) function-keys +4 (case002/004/005/006) function-spread +2 (case000/003) function-lookup +1 (case003) missing-paths +1 (case004) numeric-operators +3 (case012/013/016) object-constructor +3 (case014/023/024) performance +1 (case000) simple-array-selectors +1 (case014) wildcards +1 (case008) Zero regressions. Guard + 448 unit tests green. Co-Authored-By: Claude Sonnet 4.6 --- spec/jsonata-suite/baseline.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/jsonata-suite/baseline.lua b/spec/jsonata-suite/baseline.lua index 490934e..e473789 100644 --- a/spec/jsonata-suite/baseline.lua +++ b/spec/jsonata-suite/baseline.lua @@ -112,7 +112,11 @@ return { ["comparison-operators/deep-equals/7"] = true, ["comparison-operators/deep-equals/8"] = true, ["comparison-operators/deep-equals/9"] = true, + ["conditionals/case000"] = true, + ["conditionals/case001"] = true, ["conditionals/case002"] = true, + ["conditionals/case003"] = true, + ["conditionals/case004"] = true, ["conditionals/case005"] = true, ["conditionals/case006"] = true, ["conditionals/case007"] = true, @@ -435,6 +439,10 @@ return { ["function-join/case011"] = true, ["function-keys/case000"] = true, ["function-keys/case001"] = true, + ["function-keys/case002"] = true, + ["function-keys/case004"] = true, + ["function-keys/case005"] = true, + ["function-keys/case006"] = true, ["function-length/case000"] = true, ["function-length/case001"] = true, ["function-length/case002"] = true, @@ -454,6 +462,7 @@ return { ["function-lookup/case000"] = true, ["function-lookup/case001"] = true, ["function-lookup/case002"] = true, + ["function-lookup/case003"] = true, ["function-lowercase/case000"] = true, ["function-lowercase/case001"] = true, ["function-max/case000"] = true, @@ -645,8 +654,10 @@ return { ["function-split/case016"] = true, ["function-split/case017"] = true, ["function-split/case018"] = true, + ["function-spread/case000"] = true, ["function-spread/case001"] = true, ["function-spread/case002"] = true, + ["function-spread/case003"] = true, ["function-sqrt/case000"] = true, ["function-sqrt/case001"] = true, ["function-sqrt/case002"] = true, @@ -830,6 +841,7 @@ return { ["missing-paths/case001"] = true, ["missing-paths/case002"] = true, ["missing-paths/case003"] = true, + ["missing-paths/case004"] = true, ["missing-paths/case005"] = true, ["multiple-array-selectors/case000"] = true, ["multiple-array-selectors/case001"] = true, @@ -853,7 +865,10 @@ return { ["numeric-operators/case009"] = true, ["numeric-operators/case010"] = true, ["numeric-operators/case011"] = true, + ["numeric-operators/case012"] = true, + ["numeric-operators/case013"] = true, ["numeric-operators/case015"] = true, + ["numeric-operators/case016"] = true, ["numeric-operators/case017"] = true, ["numeric-operators/case018"] = true, ["object-constructor/case000"] = true, @@ -868,12 +883,15 @@ return { ["object-constructor/case011"] = true, ["object-constructor/case012"] = true, ["object-constructor/case013"] = true, + ["object-constructor/case014"] = true, ["object-constructor/case015"] = true, ["object-constructor/case016"] = true, ["object-constructor/case017"] = true, ["object-constructor/case018"] = true, ["object-constructor/case021"] = true, ["object-constructor/case022"] = true, + ["object-constructor/case023"] = true, + ["object-constructor/case024"] = true, ["object-constructor/case025"] = true, ["object-constructor/case026"] = true, ["parent-operator/errors/0"] = true, @@ -929,6 +947,7 @@ return { ["partial-application/case002"] = true, ["partial-application/case003"] = true, ["partial-application/case004"] = true, + ["performance/case000"] = true, ["predicates/case000"] = true, ["predicates/case001"] = true, ["predicates/case002"] = true, @@ -976,6 +995,7 @@ return { ["simple-array-selectors/case011"] = true, ["simple-array-selectors/case012"] = true, ["simple-array-selectors/case013"] = true, + ["simple-array-selectors/case014"] = true, ["simple-array-selectors/case015"] = true, ["simple-array-selectors/case016"] = true, ["simple-array-selectors/case017"] = true, @@ -1151,6 +1171,7 @@ return { ["wildcards/case004"] = true, ["wildcards/case005"] = true, ["wildcards/case006"] = true, + ["wildcards/case008"] = true, ["wildcards/case009"] = true, ["wildcards/case010/0"] = true, ["wildcards/case010/1"] = true,