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 6306438..c46bb4a 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. @@ -79,32 +72,19 @@ 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(...) - 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) @@ -174,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 @@ -182,17 +163,20 @@ 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 * 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), @@ -250,130 +234,90 @@ 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 - -- regexp() implementation. + elseif node.kind == 'func' then 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 - -- Negation. + elseif node.kind == 'unary_operation' then 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) - for i, op in ipairs(node.operators) do - local second_operand = execute_node(node.operands[i + 1], - context) - -- Sum. - if op == '+' then - prev = sum(prev, second_operand) - end - - -- Subtraction. - if op == '-' then - prev = subtract(prev, second_operand) - end - - -- Logical and. - if op == '&&' then - prev = prev and second_operand - end - - -- Logical or. - if op == '||' then - prev = prev or second_operand - end - - -- Equal. - if op == '==' then - prev = prev == second_operand - end - - -- Not equal. - if op == '!=' then - prev = prev ~= second_operand - end - - -- Greater than. - if op == '>' then - prev = prev > second_operand - end - - -- Greater or equal. - if op == '>=' then - prev = prev >= second_operand - end - - -- Lower than. - if op == '<' then - prev = prev < second_operand - end - - -- Lower or equal. - if op == '<=' then - prev = prev <= second_operand - 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) + + if op == '+' then + return sum(left, right) + elseif op == '-' then + return subtract(left, right) + 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 + elseif op == '||' then + return left or right + elseif op == '==' then + return left == right + elseif op == '!=' then + return left ~= right + elseif op == '>' then + return left > right + elseif op == '>=' then + return left >= right + elseif op == '<' then + return left < right + elseif op == '<=' then + return left <= right + else + error('Unknown binary operation: ' .. tostring(op)) end - return prev - end - - if node.kind == 'root_expression' then - return execute_node(node.expr, context) + elseif node.kind == 'evaluated' then + -- evaluated node can occurs after optimizations + return node.value + else + error('Unknown node kind: ' .. tostring(node.kind)) end end @@ -417,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) 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 10e2bd1..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. @@ -87,11 +90,12 @@ 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 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) @@ -99,12 +103,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 @@ -138,12 +140,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) @@ -151,12 +150,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) @@ -185,12 +181,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) @@ -200,69 +193,68 @@ end local ast = expressions.new('true && ($variable + 7 <= field.path_1 || ' .. '!("abc" > "abd")) && !false').ast 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' }, - { - 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 = 'const', + value_class = 'number', + value = '7' } + }, + right = { + kind = 'object_field', + path = 'field.path_1' } }, - { + right = { kind = 'unary_operation', op = '!', - node = {kind = 'const', value_class = 'bool', value = 'false'} + 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' + } } } test:is_deeply(ast, expected, 'ast_handwritten_test_1') @@ -272,113 +264,119 @@ 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_operations', - operators = {'&&'}, - operands = { - { - kind = 'binary_operations', - operators = {'||'}, - operands = { - { - kind = 'unary_operation', - op = '!', - node = { + kind = 'binary_operation', + op = '&&', + left = { + kind = 'binary_operation', + op = '||', + left = { + kind = 'unary_operation', + op = '!', + node = { + 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' + } + } +} +test:is_deeply(ast, expected, 'ast_handwritten_test_2') + +ast = expressions.new('false && "hello there" && 72.1 && $variable && ' .. + '_object1__.field && is_null($non_nil_variable) && ' .. + 'regexp("pattern", "string")').ast +expected = { + 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 = 'bool', - value = 'true' + value_class = 'number', + value = '72.1' } + }, + right = { + kind = 'variable', + name = 'variable' } }, - { - 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 = 'object_field', + path = '_object1__.field' + } + }, + right = { + kind = 'func', + name = 'is_null', + args = { + { + kind = 'variable', + name = 'non_nil_variable' } } } - } -} -test:is_deeply(ast, expected, 'ast_handwritten_test_2') - -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_operations', - operators = {'&&', '&&', '&&', '&&', '&&', '&&'}, - operands = { - { - kind = 'const', - value_class = 'bool', - value = 'false' - }, + }, + right = { + kind = 'func', + name = 'regexp', + args = { { kind = 'const', value_class = 'string', - value = 'hello there' + value = 'pattern' }, { kind = 'const', - value_class = 'number', - value = '72.1' - }, - { - kind = 'variable', - name = 'variable' - }, - { - kind = 'object_field', - path = '_object1__.field' - }, - { - kind = 'func', - name = 'is_null', - args = { - { - kind = 'variable', - name = 'non_nil_variable' - } - } - }, - { - kind = 'func', - name = 'regexp', - args = { - { - kind = 'const', - value_class = 'string', - value = 'pattern' - }, - { - kind = 'const', - value_class = 'string', - value = 'string' - } - } + value_class = 'string', + value = 'string' } } }