From 311502deb058f2d506f83fba0507f1b4d9bce04a Mon Sep 17 00:00:00 2001 From: "d.belincev" Date: Tue, 20 Jan 2026 17:07:52 +0300 Subject: [PATCH 1/5] fix: add validation and conversion types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При сравнении числа с булево, выдавало ошибку т.к. tonumber не мог привести его к числу. Добавил проверки на типы, преобразование булева в число и обратно. Исключил арифметические операции с булево и сравнения с строковым типом. --- jsonpath.lua | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/jsonpath.lua b/jsonpath.lua index 07bf2c9..b3d1b35 100755 --- a/jsonpath.lua +++ b/jsonpath.lua @@ -297,9 +297,18 @@ local function eval_ast(ast, obj) -- null must never be equal to other boolean, invert op1 return not op1 else + if type(op2) == 'string' then + return {} + end + if type(op2) == 'number' then + return op2 ~= 0 + end return (op2 and true or false) end elseif type(op1) == 'number' then + if type(op2) == 'boolean' then + return op2 and 1 or 0 + end return tonumber(op2) elseif type(op1) == 'cdata' and tostring(ffi.typeof(op1)) == 'ctype' then return tonumber(op2) @@ -314,6 +323,10 @@ local function eval_ast(ast, obj) return op1 and true or false end + local function is_bool(val) + return type(val) == 'boolean' + end + -- Helper helper: evaluate variable expression inside abstract syntax tree local function eval_var(expr, obj) if obj == nil then @@ -408,14 +421,29 @@ local function eval_ast(ast, obj) return nil, err end if operator == '+' then + if is_bool(op1) or is_bool(op2) then + return nil, "Prohibited arithmetic op on bool" + end op1 = tonumber(op1) + tonumber(op2) elseif operator == '-' then + if is_bool(op1) or is_bool(op2) then + return nil, "Prohibited arithmetic op on bool" + end op1 = tonumber(op1) - tonumber(op2) elseif operator == '*' then + if is_bool(op1) or is_bool(op2) then + return nil, "Prohibited arithmetic op on bool" + end op1 = tonumber(op1) * tonumber(op2) elseif operator == '/' then + if is_bool(op1) or is_bool(op2) then + return nil, "Prohibited arithmetic op on bool" + end op1 = tonumber(op1) / tonumber(op2) elseif operator == '%' then + if is_bool(op1) or is_bool(op2) then + return nil, "Prohibited arithmetic op on bool" + end op1 = tonumber(op1) % tonumber(op2) elseif operator:upper() == 'AND' or operator == '&&' then op1 = notempty(op1) and notempty(op2) @@ -426,22 +454,22 @@ local function eval_ast(ast, obj) elseif operator == '<>' or operator == '!=' then op1 = op1 ~= match_type(op1, op2) elseif operator == '>' then - if is_null(op1) then + if is_null(op1) or is_bool(op1) then return false end op1 = op1 > match_type(op1, op2) elseif operator == '>=' then - if is_null(op1) then + if is_null(op1) or is_bool(op1) then return false end op1 = op1 >= match_type(op1, op2) elseif operator == '<' then - if is_null(op1) then + if is_null(op1) or is_bool(op1) then return false end op1 = op1 < match_type(op1, op2) elseif operator == '<=' then - if is_null(op1) then + if is_null(op1) or is_bool(op1) then return false end op1 = op1 <= match_type(op1, op2) From 0cb6c491910f3a158050f108a0e8966f3898702e Mon Sep 17 00:00:00 2001 From: "d.belincev" Date: Wed, 21 Jan 2026 16:44:12 +0300 Subject: [PATCH 2/5] tests --- test/test.lua | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/test.lua b/test/test.lua index ff565de..d392ec8 100755 --- a/test/test.lua +++ b/test/test.lua @@ -946,6 +946,112 @@ testQuery = { lu.assertNil(err) lu.assertItemsEquals(result, { array[2], array[3] }) end, + + testFilterIntBoolComparison = function () + local array = { + { id = 1, value = 0 }, + { id = 2, value = 1 }, + { id = 3, value = 2 }, + } + local result, err = jp.query(array, '$[?(@.value==true)]') + lu.assertNil(err) + lu.assertItemsEquals(result, { array[2] }) + + local result, err = jp.query(array, '$[?(@.value>true)]') + lu.assertNil(err) + lu.assertItemsEquals(result, { array[3] }) + + local result, err = jp.query(array, '$[?(@.value>=true)]') + lu.assertNil(err) + lu.assertItemsEquals(result, { array[2], array[3] }) + + local result, err = jp.query(array, '$[?(@.value1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value>=1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value<1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value<=1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + end, + + testFilterBoolStrComparison = function () + local array = { + { id = 1, value = true }, + { id = 2, value = false }, + } + local result, err = jp.query(array, '$[?(@.value=="1")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value>"1")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value>="1")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value<"1")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value<="1")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + end, + + testFilterArithmeticOpOnBool = function () + local array = { + { id = 1, value = 0 }, + { id = 1, value = 1 }, + { id = 2, value = 2 }, + } + local result, err = jp.query(array, '$[?(@.value==true+1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value==true*1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value==true/1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value==true%1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value<>false+1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + end, } From 34b3a33f45bde81f568ce648693a3386aec5b36a Mon Sep 17 00:00:00 2001 From: "d.belincev" Date: Fri, 23 Jan 2026 14:36:53 +0300 Subject: [PATCH 3/5] fix: arihmetic op, redesign comporation --- jsonpath.lua | 119 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/jsonpath.lua b/jsonpath.lua index b3d1b35..ab526f7 100755 --- a/jsonpath.lua +++ b/jsonpath.lua @@ -291,31 +291,46 @@ end)() local function eval_ast(ast, obj) -- Helper helper: match type of second operand to type of first operand - local function match_type(op1, op2) + local function match_cmp_type(op1, op2, compare) if type(op1) == 'boolean' then if is_null(op2) then - -- null must never be equal to other boolean, invert op1 - return not op1 + return not op1, nil else - if type(op2) == 'string' then - return {} + if type(op2) == 'string' then + return nil, "cannot compare boolean with string" end if type(op2) == 'number' then - return op2 ~= 0 + if compare then + return nil, "cannot compare boolean with number" + end + return op2 ~= 0, nil + end + if type(op2) == 'boolean' then + return op2, nil end - return (op2 and true or false) + return (op2 and true or false), nil end elseif type(op1) == 'number' then if type(op2) == 'boolean' then - return op2 and 1 or 0 + return op2 and 1 or 0, nil + end + if type(op2) == 'string' then + local num = tonumber(op2) + if num == nil then + return nil, "cannot compare number with non-numeric string" + end + return num, nil end - return tonumber(op2) + return tonumber(op2), nil elseif type(op1) == 'cdata' and tostring(ffi.typeof(op1)) == 'ctype' then - return tonumber(op2) + return tonumber(op2), nil elseif is_null(op1) then - return op2 + if compare then + return nil, "cannot compare null with other values" + end + return op2, nil end - return tostring(op2 or '') + return tostring(op2 or ''), nil end -- Helper helper: convert operand to boolean @@ -323,8 +338,10 @@ local function eval_ast(ast, obj) return op1 and true or false end - local function is_bool(val) - return type(val) == 'boolean' + local function is_str_or_int(val) + return type(val) == 'string' or + type(val) == 'number' or + (type(val) == 'cdata' and tostring(ffi.typeof(val)) == 'ctype') end -- Helper helper: evaluate variable expression inside abstract syntax tree @@ -421,28 +438,34 @@ local function eval_ast(ast, obj) return nil, err end if operator == '+' then - if is_bool(op1) or is_bool(op2) then - return nil, "Prohibited arithmetic op on bool" + if is_str_or_int(op1) and is_str_or_int(op2) then + op1 = tonumber(op1) + tonumber(op2) + else + return nil, "Only operations on strings and numbers are allowed." end - op1 = tonumber(op1) + tonumber(op2) elseif operator == '-' then - if is_bool(op1) or is_bool(op2) then - return nil, "Prohibited arithmetic op on bool" + if is_str_or_int(op1) and is_str_or_int(op2) then + op1 = tonumber(op1) - tonumber(op2) + else + return nil, "Only operations on strings and numbers are allowed." end - op1 = tonumber(op1) - tonumber(op2) elseif operator == '*' then - if is_bool(op1) or is_bool(op2) then - return nil, "Prohibited arithmetic op on bool" + if is_str_or_int(op1) and is_str_or_int(op2) then + op1 = tonumber(op1) * tonumber(op2) + else + return nil, "Only operations on strings and numbers are allowed." end - op1 = tonumber(op1) * tonumber(op2) elseif operator == '/' then - if is_bool(op1) or is_bool(op2) then - return nil, "Prohibited arithmetic op on bool" + if is_str_or_int(op1) and is_str_or_int(op2) then + op1 = tonumber(op1) / tonumber(op2) + else + return nil, "Only operations on strings and numbers are allowed." end - op1 = tonumber(op1) / tonumber(op2) elseif operator == '%' then - if is_bool(op1) or is_bool(op2) then - return nil, "Prohibited arithmetic op on bool" + if is_str_or_int(op1) and is_str_or_int(op2) then + op1 = tonumber(op1) % tonumber(op2) + else + return nil, "Only operations on strings and numbers are allowed." end op1 = tonumber(op1) % tonumber(op2) elseif operator:upper() == 'AND' or operator == '&&' then @@ -450,29 +473,41 @@ local function eval_ast(ast, obj) elseif operator:upper() == 'OR' or operator == '||' then op1 = notempty(op1) or notempty(op2) elseif operator == '=' or operator == '==' then - op1 = op1 == match_type(op1, op2) + local op2, err = match_cmp_type(op1, op2, false) + if err then + return nil, err + end + op1 = op1 == op2 elseif operator == '<>' or operator == '!=' then - op1 = op1 ~= match_type(op1, op2) + local op2, err = match_cmp_type(op1, op2, false) + if err then + return nil, err + end + op1 = op1 ~= op2 elseif operator == '>' then - if is_null(op1) or is_bool(op1) then - return false + local op2, err = match_cmp_type(op1, op2, true) + if err then + return nil, err end - op1 = op1 > match_type(op1, op2) + op1 = op1 > op2 elseif operator == '>=' then - if is_null(op1) or is_bool(op1) then - return false + local op2, err = match_cmp_type(op1, op2, true) + if err then + return nil, err end - op1 = op1 >= match_type(op1, op2) + op1 = op1 >= op2 elseif operator == '<' then - if is_null(op1) or is_bool(op1) then - return false + local op2, err = match_cmp_type(op1, op2, true) + if err then + return nil, err end - op1 = op1 < match_type(op1, op2) + op1 = op1 < op2 elseif operator == '<=' then - if is_null(op1) or is_bool(op1) then - return false + local op2, err = match_cmp_type(op1, op2, true) + if err then + return nil, err end - op1 = op1 <= match_type(op1, op2) + op1 = op1 <= op2 else return nil, 'unknown expression operator "' .. operator .. '"' end From 57257364fdde4e2ef13a11840b06886e5ae5c17c Mon Sep 17 00:00:00 2001 From: "d.belincev" Date: Mon, 2 Feb 2026 16:57:39 +0300 Subject: [PATCH 4/5] fix: tabulation, covert str in arithmetic op --- jsonpath.lua | 33 ++++++++++++++++++++++++++------- test/test.lua | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/jsonpath.lua b/jsonpath.lua index ab526f7..fdf8bda 100755 --- a/jsonpath.lua +++ b/jsonpath.lua @@ -291,7 +291,7 @@ end)() local function eval_ast(ast, obj) -- Helper helper: match type of second operand to type of first operand - local function match_cmp_type(op1, op2, compare) + local function match_cmp_type(op1, op2, compare) if type(op1) == 'boolean' then if is_null(op2) then return not op1, nil @@ -439,35 +439,54 @@ local function eval_ast(ast, obj) end if operator == '+' then if is_str_or_int(op1) and is_str_or_int(op2) then - op1 = tonumber(op1) + tonumber(op2) + local num1, num2 = tonumber(op1), tonumber(op2) + if not (num1 and num2) then + return nil, "Cannot perform arithmetic on non-numeric strings" + end + op1 = num1 + num2 else return nil, "Only operations on strings and numbers are allowed." end elseif operator == '-' then if is_str_or_int(op1) and is_str_or_int(op2) then - op1 = tonumber(op1) - tonumber(op2) + local num1, num2 = tonumber(op1), tonumber(op2) + if not (num1 and num2) then + return nil, "Cannot perform arithmetic on non-numeric strings" + end + op1 = num1 - num2 else return nil, "Only operations on strings and numbers are allowed." end elseif operator == '*' then if is_str_or_int(op1) and is_str_or_int(op2) then - op1 = tonumber(op1) * tonumber(op2) + local num1, num2 = tonumber(op1), tonumber(op2) + if not (num1 and num2) then + return nil, "Cannot perform arithmetic on non-numeric strings" + end + op1 = num1 * num2 else return nil, "Only operations on strings and numbers are allowed." end elseif operator == '/' then if is_str_or_int(op1) and is_str_or_int(op2) then - op1 = tonumber(op1) / tonumber(op2) + local num1, num2 = tonumber(op1), tonumber(op2) + if not (num1 and num2) then + return nil, "Cannot perform arithmetic on non-numeric strings" + end + op1 = num1 / num2 else return nil, "Only operations on strings and numbers are allowed." end elseif operator == '%' then if is_str_or_int(op1) and is_str_or_int(op2) then - op1 = tonumber(op1) % tonumber(op2) + local num1, num2 = tonumber(op1), tonumber(op2) + if not (num1 and num2) then + return nil, "Cannot perform arithmetic on non-numeric strings" + end + op1 = num1 % num2 else return nil, "Only operations on strings and numbers are allowed." end - op1 = tonumber(op1) % tonumber(op2) elseif operator:upper() == 'AND' or operator == '&&' then op1 = notempty(op1) and notempty(op2) elseif operator:upper() == 'OR' or operator == '||' then diff --git a/test/test.lua b/test/test.lua index d392ec8..05aa4f2 100755 --- a/test/test.lua +++ b/test/test.lua @@ -1052,6 +1052,24 @@ testQuery = { lu.assertNil(err) lu.assertItemsEquals(result, {}) end, + + testFilterArithmeticOp = function () + local array = { + { id = 1, value = 0 }, + { id = 1, value = "a" }, + } + local result, err = jp.query(array, '$[?(@.value=="a"+"b")]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value=="a"+null)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + + local result, err = jp.query(array, '$[?(@.value=="a"+1)]') + lu.assertNil(err) + lu.assertItemsEquals(result, {}) + end, } From d129e2176525df7f83f96e809fb65e5f40cda119 Mon Sep 17 00:00:00 2001 From: Ilya Potemin Date: Thu, 5 Feb 2026 12:38:24 +0300 Subject: [PATCH 5/5] fix: separate operand cast strategies for different binary operator types Streamlines type cast logic depending on 4 different operator type - arithmetic, logical, equality and comparison --- jsonpath.lua | 420 +++++++++++++++++++++++++++++++++----------------- test/test.lua | 6 +- 2 files changed, 280 insertions(+), 146 deletions(-) diff --git a/jsonpath.lua b/jsonpath.lua index fdf8bda..eae49d0 100755 --- a/jsonpath.lua +++ b/jsonpath.lua @@ -286,63 +286,280 @@ local jsonpath_grammer = (function() return jsonpath end)() +--- @alias Operator 1|2|3|4|5|6|7|8|9|10|11|12|13 +--- @alias OperatorType 1|2|3|4 + +--- @type Operator[] +local OPERATORS = { + --- Arithmetic operators + ADD = 1, + SUB = 2, + MUL = 3, + DIV = 4, + MOD = 5, + --- Logical operators + AND = 6, + OR = 7, + --- Equality + EQ = 8, + NEQ = 9, + --- Comparison + GT = 10, + GTE = 11, + LT = 12, + LTE = 13, +} + +local OPERATORS_LEN = 0; +for k, v in pairs(OPERATORS) do + OPERATORS_LEN = OPERATORS_LEN + 1 +end --- Helper: evaluate abstract syntax tree. Called recursively. -local function eval_ast(ast, obj) +local OPERATOR_TYPES = { + ARITHMETIC = 1, + LOGICAL = 2, + EQUALITY = 3, + COMPARISON = 4, +} + +--- @param op Operator|number +--- @return OperatorType|0 op_type +local function get_operator_type(op) + if op >= 1 and op <= 5 then + return OPERATOR_TYPES.ARITHMETIC + elseif op == 6 or op == 7 then + return OPERATOR_TYPES.LOGICAL + elseif op == 8 or op == 9 then + return OPERATOR_TYPES.EQUALITY + elseif op >= 10 and op <= 13 then + return OPERATOR_TYPES.COMPARISON + end + return 0 +end - -- Helper helper: match type of second operand to type of first operand - local function match_cmp_type(op1, op2, compare) - if type(op1) == 'boolean' then - if is_null(op2) then - return not op1, nil - else - if type(op2) == 'string' then - return nil, "cannot compare boolean with string" - end - if type(op2) == 'number' then - if compare then - return nil, "cannot compare boolean with number" - end - return op2 ~= 0, nil - end - if type(op2) == 'boolean' then - return op2, nil - end - return (op2 and true or false), nil +--- @type { [Operator]: fun(any,any): any } +local OPERATORS_FN = { + --- Arithmetic + function(l, r) + return l + r + end, + function(l, r) + return l - r + end, + function(l, r) + return l * r + end, + function(l, r) + return l / r + end, + function(l, r) + return l % r + end, + --- Logic (boolean operands only) + function(l, r) + return l and r + end, + function(l, r) + return l or r + end, + --- Eq + function (l, r) + return l == r + end, + function (l, r) + return l ~= r + end, + --- Cmp + function (l, r) + return l > r + end, + function (l, r) + return l >= r + end, + function (l, r) + return l < r + end, + function (l, r) + return l <= r + end, +} + +assert( + OPERATORS_LEN == #OPERATORS_FN, + ("len of operators must match len of operator fns (%s vs %s)"):format(OPERATORS_LEN, #OPERATORS_FN) +) + +--- @return Operator | 0 +local function parse_operator(op) + if op == '+' then + return OPERATORS.ADD + elseif op == '-' then + return OPERATORS.SUB + elseif op == '*' then + return OPERATORS.MUL + elseif op == '/' then + return OPERATORS.DIV + elseif op == '%' then + return OPERATORS.MOD + elseif op:upper() == 'AND' or op == '&&' then + return OPERATORS.AND + elseif op:upper() == 'OR' or op == '||' then + return OPERATORS.OR + elseif op == '=' or op == '==' then + return OPERATORS.EQ + elseif op == '<>' or op == '!=' then + return OPERATORS.NEQ + elseif op == '>' then + return OPERATORS.GT + elseif op == '>=' then + return OPERATORS.GTE + elseif op == '<' then + return OPERATORS.LT + elseif op == '<=' then + return OPERATORS.LTE + else + return 0 + end +end + +--- Computes type casts and executes binary operator +--- +--- @param op Operator Operator to execute +--- @param lval any Left value of binary operator +--- @param rval any Right value of binary operator +--- @param op_str string String representation of operator, used in error reporting +--- @return any|nil val Result value +--- @return nil|string err Error, if cast has failed +local function exec_binary_op(op, lval, rval, op_str) + local l_type = type(lval) + local r_type = type(rval) + local op_type = get_operator_type(op) + + -- convert these long int numbers to normal numbers + if l_type == 'cdata' and lval ~= NULL and tostring(ffi.typeof(lval)) == 'ctype' then + l_type = "number" + lval = tonumber(lval) + end + if r_type == 'cdata' and lval ~= NULL and tostring(ffi.typeof(rval)) == 'ctype' then + r_type = "number" + rval = tonumber(rval) + end + + if op_type == OPERATOR_TYPES.ARITHMETIC then + -- arithmetic ops allowed only on numbers + if l_type == "string" then + lval = tonumber(lval) + if lval == nil then + return nil, ("can not parse string lvalue as number for operation %s"):format(op_str) end - elseif type(op1) == 'number' then - if type(op2) == 'boolean' then - return op2 and 1 or 0, nil + elseif l_type ~= "number" then + return nil, ("lvalue is not a number for operation %s"):format(op_str) + end + if r_type == "string" then + rval = tonumber(rval) + if rval == nil then + return nil, ("can not parse string rvalue as number for operation %s"):format(op_str) end - if type(op2) == 'string' then - local num = tonumber(op2) - if num == nil then - return nil, "cannot compare number with non-numeric string" - end - return num, nil + elseif r_type ~= "number" then + return nil, ("rvalue is not a number for operation %s"):format(op_str) + end + elseif op_type == OPERATOR_TYPES.LOGICAL then + -- everything which is not null is a true boolean + if l_type ~= "boolean" then + lval = not is_null(lval) + end + if r_type ~= "boolean" then + rval = not is_null(rval) + end + elseif op_type == OPERATOR_TYPES.EQUALITY then + -- cast numbers and booleans to string, if other operand is string + if l_type == "string" and r_type == "number" then + r_type = "string" + rval = tostring(rval) + elseif l_type == "string" and r_type == "boolean" then + r_type = "string" + rval = tostring(rval) + end + if r_type == "string" and l_type == "number" then + l_type = "string" + lval = tostring(lval) + elseif r_type == "string" and l_type == "boolean" then + l_type = "string" + lval = tostring(lval) + end + + -- cast booleans as numbers + if l_type == "number" and r_type == "boolean" then + r_type = "number" + rval = rval and 1 or 0 + end + if r_type == "number" and l_type == "boolean" then + l_type = "number" + lval = lval and 1 or 0 + end + + -- special comparisons when lvalue or rvalue is null + local lval_is_null, rval_is_null = is_null(lval), is_null(rval) + if lval_is_null and rval_is_null then + -- null == null -> true + return op == OPERATORS.EQ + end + if rval_is_null or lval_is_null then + -- something == null -> false + -- something != null -> true + -- null == something -> false + -- null != something -> true + return not (op == OPERATORS.EQ) + end + + -- bypass default operator functions for non-matching types + if l_type ~= r_type and op == OPERATORS.EQ then + -- values of different types are never equal + return false, nil + elseif l_type ~= r_type and op == OPERATORS.NEQ then + -- values of different types are always not equal + return true, nil + end + elseif op_type == OPERATOR_TYPES.COMPARISON then + -- allow to compare numbers with booleans + if l_type == "number" and r_type == "boolean" then + r_type = "number" + rval = rval and 1 or 0 + end + if r_type == "number" and l_type == "boolean" then + l_type = "number" + lval = lval and 1 or 0 + end + + -- try to parse string as number, if other operand is number + if l_type == "number" and r_type == "string" then + local num_rval = tonumber(rval) + if num_rval ~= nil then + r_type = "number" + rval = num_rval end - return tonumber(op2), nil - elseif type(op1) == 'cdata' and tostring(ffi.typeof(op1)) == 'ctype' then - return tonumber(op2), nil - elseif is_null(op1) then - if compare then - return nil, "cannot compare null with other values" + end + if r_type == "number" and l_type == "string" then + local num_lval = tonumber(lval) + if num_lval ~= nil then + l_type = "number" + lval = num_lval end - return op2, nil end - return tostring(op2 or ''), nil - end - -- Helper helper: convert operand to boolean - local function notempty(op1) - return op1 and true or false + -- must be the same type + if l_type ~= r_type then + return nil, ("can not apply %s on types %s and %s"):format(op_str, l_type, r_type) + end + else + return nil, ("unknown operator %s"):format(op_str) end - local function is_str_or_int(val) - return type(val) == 'string' or - type(val) == 'number' or - (type(val) == 'cdata' and tostring(ffi.typeof(val)) == 'ctype') - end + return OPERATORS_FN[op](lval, rval), nil +end + +-- Helper: evaluate abstract syntax tree. Called recursively. +local function eval_ast(ast, obj) -- Helper helper: evaluate variable expression inside abstract syntax tree local function eval_var(expr, obj) @@ -429,107 +646,24 @@ local function eval_ast(ast, obj) return nil, err end for i = 3, #expr, 2 do - local operator = expr[i] - if operator == nil then + local op_str = expr[i] + if op_str == nil then return nil, 'missing expression operator' end - local op2, err = eval_ast(expr[i + 1], obj) + local op2, eval_err = eval_ast(expr[i + 1], obj) if is_nil(op2) then - return nil, err + return nil, eval_err end - if operator == '+' then - if is_str_or_int(op1) and is_str_or_int(op2) then - local num1, num2 = tonumber(op1), tonumber(op2) - if not (num1 and num2) then - return nil, "Cannot perform arithmetic on non-numeric strings" - end - op1 = num1 + num2 - else - return nil, "Only operations on strings and numbers are allowed." - end - elseif operator == '-' then - if is_str_or_int(op1) and is_str_or_int(op2) then - local num1, num2 = tonumber(op1), tonumber(op2) - if not (num1 and num2) then - return nil, "Cannot perform arithmetic on non-numeric strings" - end - op1 = num1 - num2 - else - return nil, "Only operations on strings and numbers are allowed." - end - elseif operator == '*' then - if is_str_or_int(op1) and is_str_or_int(op2) then - local num1, num2 = tonumber(op1), tonumber(op2) - if not (num1 and num2) then - return nil, "Cannot perform arithmetic on non-numeric strings" - end - op1 = num1 * num2 - else - return nil, "Only operations on strings and numbers are allowed." - end - elseif operator == '/' then - if is_str_or_int(op1) and is_str_or_int(op2) then - local num1, num2 = tonumber(op1), tonumber(op2) - if not (num1 and num2) then - return nil, "Cannot perform arithmetic on non-numeric strings" - end - op1 = num1 / num2 - else - return nil, "Only operations on strings and numbers are allowed." - end - elseif operator == '%' then - if is_str_or_int(op1) and is_str_or_int(op2) then - local num1, num2 = tonumber(op1), tonumber(op2) - if not (num1 and num2) then - return nil, "Cannot perform arithmetic on non-numeric strings" - end - op1 = num1 % num2 - else - return nil, "Only operations on strings and numbers are allowed." - end - elseif operator:upper() == 'AND' or operator == '&&' then - op1 = notempty(op1) and notempty(op2) - elseif operator:upper() == 'OR' or operator == '||' then - op1 = notempty(op1) or notempty(op2) - elseif operator == '=' or operator == '==' then - local op2, err = match_cmp_type(op1, op2, false) - if err then - return nil, err - end - op1 = op1 == op2 - elseif operator == '<>' or operator == '!=' then - local op2, err = match_cmp_type(op1, op2, false) - if err then - return nil, err - end - op1 = op1 ~= op2 - elseif operator == '>' then - local op2, err = match_cmp_type(op1, op2, true) - if err then - return nil, err - end - op1 = op1 > op2 - elseif operator == '>=' then - local op2, err = match_cmp_type(op1, op2, true) - if err then - return nil, err - end - op1 = op1 >= op2 - elseif operator == '<' then - local op2, err = match_cmp_type(op1, op2, true) - if err then - return nil, err - end - op1 = op1 < op2 - elseif operator == '<=' then - local op2, err = match_cmp_type(op1, op2, true) - if err then - return nil, err - end - op1 = op1 <= op2 - else - return nil, 'unknown expression operator "' .. operator .. '"' + local op = parse_operator(op_str) + if op == 0 then + return nil, "unknown operator" + end + --- @cast op Operator + local result, cast_err = exec_binary_op(op, op1, op2, op_str) + if cast_err ~= nil then + return nil, cast_err end + op1 = result end return op1 end diff --git a/test/test.lua b/test/test.lua index 05aa4f2..84416a0 100755 --- a/test/test.lua +++ b/test/test.lua @@ -989,15 +989,15 @@ testQuery = { local result, err = jp.query(array, '$[?(@.value>=1)]') lu.assertNil(err) - lu.assertItemsEquals(result, {}) + lu.assertItemsEquals(result, { array[1] }) local result, err = jp.query(array, '$[?(@.value<1)]') lu.assertNil(err) - lu.assertItemsEquals(result, {}) + lu.assertItemsEquals(result, { array[2] }) local result, err = jp.query(array, '$[?(@.value<=1)]') lu.assertNil(err) - lu.assertItemsEquals(result, {}) + lu.assertItemsEquals(result, { array[1], array[2] }) end, testFilterBoolStrComparison = function ()