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, diff --git a/spec/m6a_spec.lua b/spec/m6a_spec.lua new file mode 100644 index 0000000..fa3ab9f --- /dev/null +++ b/spec/m6a_spec.lua @@ -0,0 +1,116 @@ +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) + +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) + +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) + +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) + +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 0aabf96..3eb81e6 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,14 @@ 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 + 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 @@ -765,11 +781,22 @@ 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) - 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 + 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 + V.obj_set(obj, k, val) + end + end end return obj elseif t == "function" then diff --git a/src/jsonata/functions/object.lua b/src/jsonata/functions/object.lua index 85edee4..f8f5f26 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,11 @@ R.spread = H.def(function(x) spread_obj(x[i]) end end + else + return x -- scalar: jsonata functionSpread echoes the argument unchanged + end + if #out == 0 then + return V.NOTHING end return out end, 1, 1, ">") 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