From ee72e3df6b95e0d49fe2201b826949e0407dfc9d Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Tue, 25 Sep 2018 05:55:17 +0300 Subject: [PATCH 1/5] expressions: code clean up --- graphql/expressions.lua | 129 ++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 79 deletions(-) diff --git a/graphql/expressions.lua b/graphql/expressions.lua index 6306438..cdc5fed 100644 --- a/graphql/expressions.lua +++ b/graphql/expressions.lua @@ -250,130 +250,101 @@ local function execute_node(node, context) if node.kind == 'const' then if node.value_class == 'string' then return node.value - end - - if node.value_class == 'bool' then + elseif node.value_class == 'bool' then if node.value == 'false' then return false + elseif node.value == 'true' then + return true + else + error('Unknown boolean node value: ' .. tostring(node.value)) end - return true - end - - if node.value_class == 'number' then + elseif node.value_class == 'number' then return tonumber(node.value) + else + error('Unknown const class: ' .. tostring(node.value_class)) end - end - - if node.kind == 'variable' then + elseif node.kind == 'variable' then local name = node.name return context.variables[name] - end - - if node.kind == 'object_field' then + elseif node.kind == 'object_field' then local path = node.path local field = context.object - local table_path = (path:split('.')) + local table_path = path:split('.') for i = 1, #table_path do field = field[table_path[i]] end return field - end - - if node.kind == 'func' then + elseif node.kind == 'func' then -- regexp() implementation. if node.name == 'regexp' then return utils.regexp(execute_node(node.args[1], context), execute_node(node.args[2], context)) - end - -- is_null() implementation. - if node.name == 'is_null' then + elseif node.name == 'is_null' then return execute_node(node.args[1], context) == nil - end - -- is_not_null() implementation. - if node.name == 'is_not_null' then + elseif node.name == 'is_not_null' then return execute_node(node.args[1], context) ~= nil + else + error('Unknown func name: ' .. tostring(node.name)) end - end - - if node.kind == 'unary_operation' then + elseif node.kind == 'unary_operation' then -- Negation. if node.op == '!' then return not execute_node(node.node, context) - end - -- Unary '+'. - if node.op == '+' then + elseif node.op == '+' then return execute_node(node.node, context) - end - -- Unary '-'. - if node.op == '-' then + elseif node.op == '-' then return -execute_node(node.node, context) + else + error('Unknown unary operation: ' .. tostring(node.op)) end - end - - if node.kind == 'binary_operations' then - local prev = execute_node(node.operands[1], context) + elseif node.kind == 'binary_operations' then + local acc = execute_node(node.operands[1], context) for i, op in ipairs(node.operators) do - local second_operand = execute_node(node.operands[i + 1], - context) + local right = execute_node(node.operands[i + 1], context) + -- Sum. if op == '+' then - prev = sum(prev, second_operand) - end - + acc = sum(acc, right) -- Subtraction. - if op == '-' then - prev = subtract(prev, second_operand) - end - + elseif op == '-' then + acc = subtract(acc, right) -- Logical and. - if op == '&&' then - prev = prev and second_operand - end - + elseif op == '&&' then + acc = acc and right -- Logical or. - if op == '||' then - prev = prev or second_operand - end - + elseif op == '||' then + acc = acc or right -- Equal. - if op == '==' then - prev = prev == second_operand - end - + elseif op == '==' then + acc = acc == right -- Not equal. - if op == '!=' then - prev = prev ~= second_operand - end - + elseif op == '!=' then + acc = acc ~= right -- Greater than. - if op == '>' then - prev = prev > second_operand - end - + elseif op == '>' then + acc = acc > right -- Greater or equal. - if op == '>=' then - prev = prev >= second_operand - end - + elseif op == '>=' then + acc = acc >= right -- Lower than. - if op == '<' then - prev = prev < second_operand - end - + elseif op == '<' then + acc = acc < right -- Lower or equal. - if op == '<=' then - prev = prev <= second_operand + elseif op == '<=' then + acc = acc <= right + else + error('Unknown binary operation: ' .. tostring(op)) end end - return prev - end - - if node.kind == 'root_expression' then + return acc + elseif node.kind == 'root_expression' then return execute_node(node.expr, context) + else + error('Unknown node kind: ' .. tostring(node.kind)) end end From 173172d91237598b088b13c79341e779c4e58707 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Thu, 27 Sep 2018 04:39:40 +0300 Subject: [PATCH 2/5] expression: simplify node format for a binary op It will simplify optimization passes (say, constant propagation) and finding suitable indexes (which will involve DNF constructing) in the future. --- graphql/expressions.lua | 101 ++++++------ test/unit/expr_tree.test.lua | 289 ++++++++++++++++++----------------- 2 files changed, 199 insertions(+), 191 deletions(-) diff --git a/graphql/expressions.lua b/graphql/expressions.lua index cdc5fed..d0cc83f 100644 --- a/graphql/expressions.lua +++ b/graphql/expressions.lua @@ -86,25 +86,19 @@ local function root_expr_node(expr) } end +-- left associativity local function bin_op_node(...) - if select('#', ...) == 1 then + local args_cnt = select('#', ...) + assert(args_cnt % 2 == 1) + if args_cnt == 1 then return select(1, ...) end - local operators = {} - local operands = {} - for i = 1, select('#', ...) do - local v = select(i, ...) - if i % 2 == 0 then - table.insert(operators, v) - else - table.insert(operands, v) - end - end - return { - kind = 'binary_operations', - operators = operators, - operands = operands - } + return bin_op_node({ + kind = 'binary_operation', + op = select(2, ...), + left = select(1, ...), + right = select(3, ...), + }, select(4, ...)) end local function unary_op_node(unary_operator, operand_1) @@ -301,44 +295,43 @@ local function execute_node(node, context) else error('Unknown unary operation: ' .. tostring(node.op)) end - elseif node.kind == 'binary_operations' then - local acc = execute_node(node.operands[1], context) - for i, op in ipairs(node.operators) do - local right = execute_node(node.operands[i + 1], context) - - -- Sum. - if op == '+' then - acc = sum(acc, right) - -- Subtraction. - elseif op == '-' then - acc = subtract(acc, right) - -- Logical and. - elseif op == '&&' then - acc = acc and right - -- Logical or. - elseif op == '||' then - acc = acc or right - -- Equal. - elseif op == '==' then - acc = acc == right - -- Not equal. - elseif op == '!=' then - acc = acc ~= right - -- Greater than. - elseif op == '>' then - acc = acc > right - -- Greater or equal. - elseif op == '>=' then - acc = acc >= right - -- Lower than. - elseif op == '<' then - acc = acc < right - -- Lower or equal. - elseif op == '<=' then - acc = acc <= right - else - error('Unknown binary operation: ' .. tostring(op)) - end + elseif node.kind == 'binary_operation' then + local op = node.op + local left = execute_node(node.left, context) + local right = execute_node(node.right, context) + + -- Sum. + if op == '+' then + return sum(left, right) + -- Subtraction. + elseif op == '-' then + return subtract(left, right) + -- Logical and. + elseif op == '&&' then + return left and right + -- Logical or. + elseif op == '||' then + return left or right + -- Equal. + elseif op == '==' then + return left == right + -- Not equal. + elseif op == '!=' then + return left ~= right + -- Greater than. + elseif op == '>' then + return left > right + -- Greater or equal. + elseif op == '>=' then + return left >= right + -- Lower than. + elseif op == '<' then + return left < right + -- Lower or equal. + elseif op == '<=' then + return left <= right + else + error('Unknown binary operation: ' .. tostring(op)) end return acc elseif node.kind == 'root_expression' then diff --git a/test/unit/expr_tree.test.lua b/test/unit/expr_tree.test.lua index 10e2bd1..1f90c12 100755 --- a/test/unit/expr_tree.test.lua +++ b/test/unit/expr_tree.test.lua @@ -87,9 +87,10 @@ for i_1, id_1 in ipairs(identifiers) do end local op_node = { - kind = 'binary_operations', - operators = {op}, - operands = {node_1, node_2} + kind = 'binary_operation', + op = op, + left = node_1, + right = node_2, } local expected = {kind = 'root_expression', expr = op_node} local test_name = ('ast_bin_op_1_%d.%s.%d'):format(i_1, op, i_2) @@ -202,65 +203,67 @@ local ast = expressions.new('true && ($variable + 7 <= field.path_1 || ' .. local expected = { kind = 'root_expression', expr = { - kind = 'binary_operations', - operators = {'&&', '&&'}, - operands = { - {kind = 'const', value_class = 'bool', value = 'true'}, - { - kind = 'binary_operations', - operators = {'||'}, - operands = { - { - kind = 'binary_operations', - operators = {'<='}, - operands = { - { - kind = 'binary_operations', - operators = {'+'}, - operands = { - { - kind = 'variable', - name = 'variable' - }, - { - kind = 'const', - value_class = 'number', - value = '7' - } - } - }, - { - kind = 'object_field', - path = 'field.path_1' - } + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'const', + value_class = 'bool', + value = 'true' + }, + right = { + kind = 'binary_operation', + op = '||', + left = { + kind = 'binary_operation', + op = '<=', + left = { + kind = 'binary_operation', + op = '+', + left = { + kind = 'variable', + name = 'variable' + }, + right = { + kind = 'const', + value_class = 'number', + value = '7' } }, - { - kind = 'unary_operation', - op = '!', - node = { - kind = 'binary_operations', - operators = {'>'}, - operands = { - { - kind = 'const', - value_class = 'string', - value = 'abc' - }, - { - kind = 'const', - value_class = 'string', - value = 'abd' - } - } + right = { + kind = 'object_field', + path = 'field.path_1' + } + }, + right = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'binary_operation', + op = '>', + left = { + kind = 'const', + value_class = 'string', + value = 'abc' + }, + right = { + kind = 'const', + value_class = 'string', + value = 'abd' } } } - }, - { - kind = 'unary_operation', - op = '!', - node = {kind = 'const', value_class = 'bool', value = 'false'} + } + }, + right = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'const', + value_class = 'bool', + value = 'false' } } } @@ -274,48 +277,42 @@ ast = expressions.new('(!false|| true) && (!(true) || false)').ast expected = { kind = 'root_expression', expr = { - kind = 'binary_operations', - operators = {'&&'}, - operands = { - { - kind = 'binary_operations', - operators = {'||'}, - operands = { - { - kind = 'unary_operation', - op = '!', - node = { - kind = 'const', - value_class = 'bool', - value = 'false' - } - }, - { - kind = 'const', - value_class = 'bool', - value = 'true' - } + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '||', + left = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'const', + value_class = 'bool', + value = 'false' } }, - { - kind = 'binary_operations', - operators = {'||'}, - operands = { - { - kind = 'unary_operation', - op = '!', - node = { - kind = 'const', - value_class = 'bool', - value = 'true' - } - }, - { - kind = 'const', - value_class = 'bool', - value = 'false' - } + right = { + kind = 'const', + value_class = 'bool', + value = 'true' + } + }, + right = { + kind = 'binary_operation', + op = '||', + left = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'const', + value_class = 'bool', + value = 'true' } + }, + right = { + kind = 'const', + value_class = 'bool', + value = 'false' } } } @@ -328,33 +325,51 @@ ast = expressions.new('false && "hello there" && 72.1 && $variable && ' .. expected = { kind = 'root_expression', expr = { - kind = 'binary_operations', - operators = {'&&', '&&', '&&', '&&', '&&', '&&'}, - operands = { - { - kind = 'const', - value_class = 'bool', - value = 'false' - }, - { - kind = 'const', - value_class = 'string', - value = 'hello there' - }, - { - kind = 'const', - value_class = 'number', - value = '72.1' - }, - { - kind = 'variable', - name = 'variable' - }, - { - kind = 'object_field', - path = '_object1__.field' + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'const', + value_class = 'bool', + value = 'false' + }, + right = { + kind = 'const', + value_class = 'string', + value = 'hello there' + } + }, + right = { + kind = 'const', + value_class = 'number', + value = '72.1' + } + }, + right = { + kind = 'variable', + name = 'variable' + } + }, + right = { + kind = 'object_field', + path = '_object1__.field' + } }, - { + right = { kind = 'func', name = 'is_null', args = { @@ -363,21 +378,21 @@ expected = { name = 'non_nil_variable' } } - }, - { - kind = 'func', - name = 'regexp', - args = { - { - kind = 'const', - value_class = 'string', - value = 'pattern' - }, - { - kind = 'const', - value_class = 'string', - value = 'string' - } + } + }, + right = { + kind = 'func', + name = 'regexp', + args = { + { + kind = 'const', + value_class = 'string', + value = 'pattern' + }, + { + kind = 'const', + value_class = 'string', + value = 'string' } } } From d737ed0aee8aae6a073d8c1b67fa13daf80abd11 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Thu, 27 Sep 2018 06:54:00 +0300 Subject: [PATCH 3/5] expressions: remove root node from AST It don't seems to be very useful, so why we should store it? --- graphql/expressions.lua | 12 +- test/unit/expr_tree.test.lua | 256 ++++++++++++++++------------------- 2 files changed, 119 insertions(+), 149 deletions(-) diff --git a/graphql/expressions.lua b/graphql/expressions.lua index d0cc83f..c6b3cf7 100644 --- a/graphql/expressions.lua +++ b/graphql/expressions.lua @@ -79,13 +79,6 @@ local identical_node = identical local op_name = identical -local function root_expr_node(expr) - return { - kind = 'root_expression', - expr = expr - } -end - -- left associativity local function bin_op_node(...) local args_cnt = select('#', ...) @@ -176,7 +169,7 @@ local _functions = (is_null + is_not_null + regexp) / identical -- terms of priority. local expression_grammar = P { 'init_expr', - init_expr = V('expr') * eof / root_expr_node, + init_expr = V('expr') * eof / identical_node, expr = spaces * V('log_expr_or') * spaces / identical_node, log_expr_or = V('log_expr_and') * (spaces * _logic_or * @@ -333,9 +326,6 @@ local function execute_node(node, context) else error('Unknown binary operation: ' .. tostring(op)) end - return acc - elseif node.kind == 'root_expression' then - return execute_node(node.expr, context) else error('Unknown node kind: ' .. tostring(node.kind)) end diff --git a/test/unit/expr_tree.test.lua b/test/unit/expr_tree.test.lua index 1f90c12..d912942 100755 --- a/test/unit/expr_tree.test.lua +++ b/test/unit/expr_tree.test.lua @@ -92,7 +92,7 @@ for i_1, id_1 in ipairs(identifiers) do left = node_1, right = node_2, } - local expected = {kind = 'root_expression', expr = op_node} + local expected = op_node local test_name = ('ast_bin_op_1_%d.%s.%d'):format(i_1, op, i_2) test:is_deeply(ast, expected, test_name) @@ -100,12 +100,10 @@ for i_1, id_1 in ipairs(identifiers) do second_identifier) ast = expressions.new(bin_operation).ast expected = { - kind = 'root_expression', - expr = { - kind = 'unary_operation', - op = '!', - node = op_node - } } + kind = 'unary_operation', + op = '!', + node = op_node + } test_name = ('ast_bin_op_2_%d.%s.%d'):format(i_1, op, i_2) test:is_deeply(ast, expected, test_name) end @@ -139,12 +137,9 @@ for i_1, id_1 in ipairs(identifiers) do arg_node_1 = {kind = 'object_field', path = id_1[2]} end local expected = { - kind = 'root_expression', - expr = { - kind = 'func', - name = 'is_null', - args = {arg_node_1} - } + kind = 'func', + name = 'is_null', + args = {arg_node_1} } local test_name = ('ast_func_is_null_%d'):format(i_1) test:is_deeply(ast, expected, test_name) @@ -152,12 +147,9 @@ for i_1, id_1 in ipairs(identifiers) do func = ('is_not_null( %s )'):format(first_identifier) ast = expressions.new(func).ast expected = { - kind = 'root_expression', - expr = { - kind = 'func', - name = 'is_not_null', - args = {arg_node_1} - } + kind = 'func', + name = 'is_not_null', + args = {arg_node_1} } test_name = ('ast_func_not_null_%d'):format(i_1) test:is_deeply(ast, expected, test_name) @@ -186,12 +178,9 @@ for i_1, id_1 in ipairs(identifiers) do arg_node_2 = {kind = 'object_field', path = id_2[2]} end expected = { - kind = 'root_expression', - expr = { - kind = 'func', - name = 'regexp', - args = {arg_node_1, arg_node_2} - } + kind = 'func', + name = 'regexp', + args = {arg_node_1, arg_node_2} } test_name = ('ast_func_regexp_%d.%d'):format(i_1, i_2) test:is_deeply(ast, expected, test_name) @@ -201,70 +190,67 @@ end local ast = expressions.new('true && ($variable + 7 <= field.path_1 || ' .. '!("abc" > "abd")) && !false').ast local expected = { - kind = 'root_expression', - expr = { + kind = 'binary_operation', + op = '&&', + left = { kind = 'binary_operation', op = '&&', left = { + kind = 'const', + value_class = 'bool', + value = 'true' + }, + right = { kind = 'binary_operation', - op = '&&', + op = '||', left = { - kind = 'const', - value_class = 'bool', - value = 'true' - }, - right = { kind = 'binary_operation', - op = '||', + op = '<=', left = { kind = 'binary_operation', - op = '<=', + op = '+', left = { - kind = 'binary_operation', - op = '+', - left = { - kind = 'variable', - name = 'variable' - }, - right = { - kind = 'const', - value_class = 'number', - value = '7' - } + kind = 'variable', + name = 'variable' }, right = { - kind = 'object_field', - path = 'field.path_1' + kind = 'const', + value_class = 'number', + value = '7' } }, right = { - kind = 'unary_operation', - op = '!', - node = { - kind = 'binary_operation', - op = '>', - left = { - kind = 'const', - value_class = 'string', - value = 'abc' - }, - right = { - kind = 'const', - value_class = 'string', - value = 'abd' - } + kind = 'object_field', + path = 'field.path_1' + } + }, + right = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'binary_operation', + op = '>', + left = { + kind = 'const', + value_class = 'string', + value = 'abc' + }, + right = { + kind = 'const', + value_class = 'string', + value = 'abd' } } } - }, - right = { - kind = 'unary_operation', - op = '!', - node = { - kind = 'const', - value_class = 'bool', - value = 'false' - } + } + }, + right = { + kind = 'unary_operation', + op = '!', + node = { + kind = 'const', + value_class = 'bool', + value = 'false' } } } @@ -275,45 +261,42 @@ test:is_deeply(ast, expected, 'ast_handwritten_test_1') -- amount of brackets" are actually different nodes. ast = expressions.new('(!false|| true) && (!(true) || false)').ast expected = { - kind = 'root_expression', - expr = { + kind = 'binary_operation', + op = '&&', + left = { kind = 'binary_operation', - op = '&&', + op = '||', left = { - kind = 'binary_operation', - op = '||', - left = { - kind = 'unary_operation', - op = '!', - node = { - kind = 'const', - value_class = 'bool', - value = 'false' - } - }, - right = { + kind = 'unary_operation', + op = '!', + node = { kind = 'const', value_class = 'bool', - value = 'true' + value = 'false' } }, right = { - kind = 'binary_operation', - op = '||', - left = { - kind = 'unary_operation', - op = '!', - node = { - kind = 'const', - value_class = 'bool', - value = 'true' - } - }, - right = { + kind = 'const', + value_class = 'bool', + value = 'true' + } + }, + right = { + kind = 'binary_operation', + op = '||', + left = { + kind = 'unary_operation', + op = '!', + node = { kind = 'const', value_class = 'bool', - value = 'false' + value = 'true' } + }, + right = { + kind = 'const', + value_class = 'bool', + value = 'false' } } } @@ -323,8 +306,9 @@ ast = expressions.new('false && "hello there" && 72.1 && $variable && ' .. '_object1__.field && is_null($non_nil_variable) && ' .. 'regexp("pattern", "string")').ast expected = { - kind = 'root_expression', - expr = { + kind = 'binary_operation', + op = '&&', + left = { kind = 'binary_operation', op = '&&', left = { @@ -340,62 +324,58 @@ expected = { kind = 'binary_operation', op = '&&', left = { - kind = 'binary_operation', - op = '&&', - left = { - kind = 'const', - value_class = 'bool', - value = 'false' - }, - right = { - kind = 'const', - value_class = 'string', - value = 'hello there' - } + kind = 'const', + value_class = 'bool', + value = 'false' }, right = { kind = 'const', - value_class = 'number', - value = '72.1' + value_class = 'string', + value = 'hello there' } }, right = { - kind = 'variable', - name = 'variable' + kind = 'const', + value_class = 'number', + value = '72.1' } }, right = { - kind = 'object_field', - path = '_object1__.field' + kind = 'variable', + name = 'variable' } }, right = { - kind = 'func', - name = 'is_null', - args = { - { - kind = 'variable', - name = 'non_nil_variable' - } - } + kind = 'object_field', + path = '_object1__.field' } }, right = { kind = 'func', - name = 'regexp', + name = 'is_null', args = { { - kind = 'const', - value_class = 'string', - value = 'pattern' - }, - { - kind = 'const', - value_class = 'string', - value = 'string' + kind = 'variable', + name = 'non_nil_variable' } } } + }, + right = { + kind = 'func', + name = 'regexp', + args = { + { + kind = 'const', + value_class = 'string', + value = 'pattern' + }, + { + kind = 'const', + value_class = 'string', + value = 'string' + } + } } } From dd9cbdd46aebfd3b0a75e0b9491d62f711e2c4e2 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Thu, 27 Sep 2018 06:56:36 +0300 Subject: [PATCH 4/5] expressions: add mul (*), div (/) and modulo (%) Stripped non-informative comments. --- graphql/expressions.lua | 49 ++++++++++++--------------------- test/unit/expr_execute.test.lua | 2 +- test/unit/expr_tree.test.lua | 5 +++- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/graphql/expressions.lua b/graphql/expressions.lua index c6b3cf7..9737b53 100644 --- a/graphql/expressions.lua +++ b/graphql/expressions.lua @@ -49,25 +49,18 @@ local unary_minus = P('-') local unary_plus = P('+') -- Possible binary operator patterns: --- 1) Logical and. local logic_and = P('&&') --- 2) logical or. local logic_or = P('||') --- 3) + local addition = P('+') --- 4) - local subtraction = P('-') --- 5) == +local multiplication = P('*') +local division = P('/') +local modulo = P('%') local eq = P('==') --- 6) != local not_eq = P('!=') --- 7) > local gt = P('>') --- 8) >= local ge = P('>=') --- 9) < local lt = P('<') --- 10) <= local le = P('<=') -- AST nodes generating functions. @@ -161,7 +154,8 @@ local _literal = _bool + _number + _string local _logic_or = logic_or / op_name local _logic_and = logic_and / op_name local _comparison_op = (eq + not_eq + ge + gt + le + lt) / op_name -local _arithmetic_op = (addition + subtraction) / op_name +local _arithmetic_sum_op = (addition + subtraction) / op_name +local _arithmetic_mul_op = (multiplication + division + modulo) / op_name local _unary_op = (negation + unary_minus + unary_plus) / op_name local _functions = (is_null + is_not_null + regexp) / identical @@ -176,10 +170,13 @@ local expression_grammar = P { spaces * V('log_expr_and')) ^ 0 / bin_op_node, log_expr_and = V('comparison') * (spaces * _logic_and * spaces * V('comparison')) ^ 0 / bin_op_node, - comparison = V('arithmetic_expr') * (spaces * _comparison_op * spaces * - V('arithmetic_expr')) ^ 0 / bin_op_node, - arithmetic_expr = V('unary_expr') * (spaces * _arithmetic_op * spaces * - V('unary_expr')) ^ 0 / bin_op_node, + comparison = V('arithmetic_sum_expr') * (spaces * _comparison_op * spaces * + V('arithmetic_sum_expr')) ^ 0 / bin_op_node, + arithmetic_sum_expr = V('arithmetic_mul_expr') * (spaces * + _arithmetic_sum_op * spaces * + V('arithmetic_mul_expr')) ^ 0 / bin_op_node, + arithmetic_mul_expr = V('unary_expr') * (spaces * _arithmetic_mul_op * + spaces * V('unary_expr')) ^ 0 / bin_op_node, unary_expr = (_unary_op * V('first_prio') / unary_op_node) + (V('first_prio') / identical_node), @@ -262,27 +259,21 @@ local function execute_node(node, context) end return field elseif node.kind == 'func' then - -- regexp() implementation. if node.name == 'regexp' then return utils.regexp(execute_node(node.args[1], context), execute_node(node.args[2], context)) - -- is_null() implementation. elseif node.name == 'is_null' then return execute_node(node.args[1], context) == nil - -- is_not_null() implementation. elseif node.name == 'is_not_null' then return execute_node(node.args[1], context) ~= nil else error('Unknown func name: ' .. tostring(node.name)) end elseif node.kind == 'unary_operation' then - -- Negation. if node.op == '!' then return not execute_node(node.node, context) - -- Unary '+'. elseif node.op == '+' then return execute_node(node.node, context) - -- Unary '-'. elseif node.op == '-' then return -execute_node(node.node, context) else @@ -293,34 +284,30 @@ local function execute_node(node, context) local left = execute_node(node.left, context) local right = execute_node(node.right, context) - -- Sum. if op == '+' then return sum(left, right) - -- Subtraction. elseif op == '-' then return subtract(left, right) - -- Logical and. + elseif op == '*' then + return left * right + elseif op == '/' then + return left / right + elseif op == '%' then + return left % right elseif op == '&&' then return left and right - -- Logical or. elseif op == '||' then return left or right - -- Equal. elseif op == '==' then return left == right - -- Not equal. elseif op == '!=' then return left ~= right - -- Greater than. elseif op == '>' then return left > right - -- Greater or equal. elseif op == '>=' then return left >= right - -- Lower than. elseif op == '<' then return left < right - -- Lower or equal. elseif op == '<=' then return left <= right else diff --git a/test/unit/expr_execute.test.lua b/test/unit/expr_execute.test.lua index b616857..2d808e5 100755 --- a/test/unit/expr_execute.test.lua +++ b/test/unit/expr_execute.test.lua @@ -28,7 +28,7 @@ local variables = { -- case: complex local case_name = 'complex' -local str = 'true && ($variable + 7 == field.path_1 && !(2399>23941)) && !false' +local str = 'true && ($variable + 1 * 7 == field.path_1 && !(2399>23941)) && !false' local exp_result = true local expr = expressions.new(str) test:is(expr:execute(object, variables), exp_result, case_name) diff --git a/test/unit/expr_tree.test.lua b/test/unit/expr_tree.test.lua index d912942..7cb9d51 100755 --- a/test/unit/expr_tree.test.lua +++ b/test/unit/expr_tree.test.lua @@ -23,6 +23,9 @@ local binary_operators = { '&&', '+', '-', + '*', + '/', + '%', '==', '!=', '>', @@ -31,7 +34,7 @@ local binary_operators = { '<=', } -test:plan(1046) +test:plan(1340) -- Bunch of tests for all the sets of binary operator and its -- operands. From ecbc2be728dd04d435fa50052ed068c6585f72ef Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Mon, 4 Feb 2019 07:02:10 +0300 Subject: [PATCH 5/5] expressions: add constant propagation pass Needed for #228. --- Makefile | 1 + graphql/accessor_general.lua | 7 + graphql/expressions.lua | 5 + graphql/expressions/constant_propagation.lua | 159 +++++++++++++++++++ test/unit/constant_propagation.test.lua | 138 ++++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 graphql/expressions/constant_propagation.lua create mode 100755 test/unit/constant_propagation.test.lua diff --git a/Makefile b/Makefile index 3f1617b..9897db4 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ lint: graphql/core/rules.lua \ graphql/core/validate_variables.lua \ graphql/convert_schema/*.lua \ + graphql/expressions/*.lua \ graphql/server/*.lua \ test/bench/*.lua \ test/space/*.lua \ diff --git a/graphql/accessor_general.lua b/graphql/accessor_general.lua index 48bae93..8232856 100644 --- a/graphql/accessor_general.lua +++ b/graphql/accessor_general.lua @@ -14,6 +14,7 @@ local db_schema_helpers = require('graphql.db_schema_helpers') local error_codes = require('graphql.error_codes') local statistics = require('graphql.statistics') local expressions = require('graphql.expressions') +local constant_propagation = require('graphql.expressions.constant_propagation') local find_index = require('graphql.find_index') local check = utils.check @@ -500,6 +501,12 @@ local function prepare_select_internal(self, collection_name, from, filter, expr = expressions.new(expr) end + -- propagate constants in the expression + if expr ~= nil then + expr = constant_propagation.transform(expr, + {variables = qcontext.variables}) + end + -- read only process_tuple options local select_opts = { limit = args.limit, diff --git a/graphql/expressions.lua b/graphql/expressions.lua index 9737b53..c46bb4a 100644 --- a/graphql/expressions.lua +++ b/graphql/expressions.lua @@ -313,6 +313,9 @@ local function execute_node(node, context) else error('Unknown binary operation: ' .. tostring(op)) end + elseif node.kind == 'evaluated' then + -- evaluated node can occurs after optimizations + return node.value else error('Unknown node kind: ' .. tostring(node.kind)) end @@ -358,4 +361,6 @@ function expressions.new(str) }) end +expressions.execute_node = execute_node + return expressions diff --git a/graphql/expressions/constant_propagation.lua b/graphql/expressions/constant_propagation.lua new file mode 100644 index 0000000..fbe263d --- /dev/null +++ b/graphql/expressions/constant_propagation.lua @@ -0,0 +1,159 @@ +local expressions = require('graphql.expressions') + +local constant_propagation = {} + +local function evaluate_node(node, context) + local value = expressions.execute_node(node, context) + return { + kind = 'evaluated', + value = value, + } +end + +--- Fold variables and constant expressions into constants. +--- +--- Introdices the new node type: +--- +--- { +--- kind = 'evaluated', +--- value = <...> (of any type), +--- } +--- +--- @tparam table current AST node (e.g. root node) +--- +--- @tparam table context table of the following values: +--- +--- * variables (table) +--- +--- @treturn table transformed AST +local function propagate_constants_internal(node, context) + if node.kind == 'const' or node.kind == 'variable' then + return evaluate_node(node, context) + elseif node.kind == 'object_field' then + return node + elseif node.kind == 'func' then + local new_args = {} + local changed = false -- at least one arg was changed + local evaluated = true -- all args were evaluated + for i = 1, #node.args do + local arg = propagate_constants_internal(node.args[i], context) + table.insert(new_args, arg) + if arg ~= node.args[i] then + changed = true + end + if arg.kind ~= 'evaluated' then + evaluated = false + end + end + if not changed then return node end + local new_node = { + kind = 'func', + name = node.name, + args = new_args, + } + if not evaluated then return new_node end + return evaluate_node(new_node, context) + elseif node.kind == 'unary_operation' then + local arg = propagate_constants_internal(node.node, context) + if arg == node.node then return node end + local new_node = { + kind = 'unary_operation', + op = node.op, + node = arg, + } + if arg.kind ~= 'evaluated' then return new_node end + return evaluate_node(new_node, context) + elseif node.kind == 'binary_operation' then + local left = propagate_constants_internal(node.left, context) + local right = propagate_constants_internal(node.right, context) + + if left == node.left and right == node.right then return node end + + -- handle the case when both args were evaluated + if left.kind == 'evaluated' and right.kind == 'evaluated' then + return evaluate_node({ + kind = 'binary_operation', + op = node.op, + left = left, + right = right, + }, context) + end + + -- handle '{false,true} {&&,||} X' and 'X {&&,||} {false,true}' + if node.op == '&&' or node.op == '||' then + local e_node -- evaluated node + local o_node -- other node + + if left.kind == 'evaluated' then + e_node = left + o_node = right + elseif right.kind == 'evaluated' then + e_node = right + o_node = left + end + + if e_node ~= nil then + assert(type(e_node.value) == 'boolean') + if node.op == '&&' and e_node.value then + -- {true && o_node, o_node && true} + return o_node + elseif node.op == '&&' and not e_node.value then + -- {false && o_node, o_node && false} + return { + kind = 'evaluated', + value = false, + } + elseif node.op == '||' and e_node.value then + -- {true || o_node, o_node || true} + return { + kind = 'evaluated', + value = true, + } + elseif node.op == '||' and not e_node.value then + -- {false || o_node, o_node || false} + return o_node + else + assert(false) + end + end + end + + return { + kind = 'binary_operation', + op = node.op, + left = left, + right = right, + } + elseif node.kind == 'evaluated' then + return node + else + error('Unknown node kind: ' .. tostring(node.kind)) + end +end + +--- Fold variables and constant expressions into constants. +--- +--- Introdices the new node type: +--- +--- { +--- kind = 'evaluated', +--- value = <...> (of any type), +--- } +--- +--- @tparam table expression +--- +--- @tparam table context table of the following values: +--- +--- * variables (table) +--- +--- @treturn table transformed expression +function constant_propagation.transform(expr, context) + local new_ast = propagate_constants_internal(expr.ast, context) + if new_ast == expr.ast then return expr end + return setmetatable({ + raw = expr.raw, + ast = new_ast, + }, getmetatable(expr)) +end + +return constant_propagation diff --git a/test/unit/constant_propagation.test.lua b/test/unit/constant_propagation.test.lua new file mode 100755 index 0000000..26fe58b --- /dev/null +++ b/test/unit/constant_propagation.test.lua @@ -0,0 +1,138 @@ +#!/usr/bin/env tarantool + +local tap = require('tap') +local fio = require('fio') +local json = require('json') + +-- require in-repo version of graphql/ sources despite current working directory +local cur_dir = fio.abspath(debug.getinfo(1).source:match("@?(.*/)") + :gsub('/./', '/'):gsub('/+$', '')) +package.path = + cur_dir .. '/../../?/init.lua' .. ';' .. + cur_dir .. '/../../?.lua' .. ';' .. + package.path + +local expressions = require('graphql.expressions') +local constant_propagation = require('graphql.expressions.constant_propagation') +local test_utils = require('test.test_utils') + +local cases = { + { + expr = 'x > 7 + $v + 1', + variables = {v = 2}, + expected_ast = { + kind = 'binary_operation', + op = '>', + left = { + kind = 'object_field', + path = 'x', + }, + right = { + kind = 'evaluated', + value = 10, + }, + }, + execute = { + { + object = {x = 10}, + expected_result = false, + }, + { + object = {x = 12}, + expected_result = true, + }, + } + }, + { + expr = 'true && x > 1', + expected_ast = { + kind = 'binary_operation', + op = '>', + left = { + kind = 'object_field', + path = 'x', + }, + right = { + kind = 'evaluated', + value = 1, + }, + }, + execute = { + { + object = {x = 1}, + expected_result = false, + }, + { + object = {x = 2}, + expected_result = true, + }, + } + }, + { + expr = 'false && x > 1', + expected_ast = { + kind = 'evaluated', + value = false, + }, + execute = { + { + object = {x = 1}, + expected_result = false, + }, + { + object = {x = 2}, + expected_result = false, + }, + } + }, + { + expr = '1 + 2 * $v == -1', + variables = {v = -1}, + expected_ast = { + kind = 'evaluated', + value = true, + }, + execute = { + { + expected_result = true, + }, + } + }, +} + +local function run_case(test, case) + test:test(case.name or case.expr, function(test) + local plan = 1 + if case.expected_ast then + plan = plan + 1 + end + plan = plan + #(case.execute or {}) + test:plan(plan) + + local compiled_expr = expressions.new(case.expr) + local context = {variables = case.variables} + local optimized_expr = constant_propagation.transform(compiled_expr, + context) + if case.expected_ast then + test:is_deeply(optimized_expr.ast, case.expected_ast, + 'optimized ast') + end + for _, e in ipairs(case.execute or {}) do + local result = optimized_expr:execute(e.object) + test:is(result, e.expected_result, 'execute with ' .. + json.encode(e.object)) + end + local optimized_expr_2 = constant_propagation.transform(optimized_expr, + context) + test:ok(optimized_expr == optimized_expr_2, 'self-applicability') + end) +end + +local test = tap.test('constant_propagation') +test:plan(#cases) + +for _, case in ipairs(cases) do + test_utils.show_trace(run_case, test, case) +end + +os.exit(test:check() == true and 0 or 1)