Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 95e3cf4

Browse files
committed
expressions: add constant propagation pass
Needed for #228.
1 parent d65b83d commit 95e3cf4

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ lint:
1313
graphql/core/rules.lua \
1414
graphql/core/validate_variables.lua \
1515
graphql/convert_schema/*.lua \
16+
graphql/expressions/*.lua \
1617
graphql/server/*.lua \
1718
test/bench/*.lua \
1819
test/space/*.lua \

graphql/accessor_general.lua

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local db_schema_helpers = require('graphql.db_schema_helpers')
1414
local error_codes = require('graphql.error_codes')
1515
local statistics = require('graphql.statistics')
1616
local expressions = require('graphql.expressions')
17+
local constant_propagation = require('graphql.expressions.constant_propagation')
1718
local find_index = require('graphql.find_index')
1819

1920
local check = utils.check
@@ -500,6 +501,12 @@ local function prepare_select_internal(self, collection_name, from, filter,
500501
expr = expressions.new(expr)
501502
end
502503

504+
-- propagate constants in the expression
505+
if expr ~= nil then
506+
expr = constant_propagation.transform(expr,
507+
{variables = qcontext.variables})
508+
end
509+
503510
-- read only process_tuple options
504511
local select_opts = {
505512
limit = args.limit,

graphql/expressions.lua

+5
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ local function execute_node(node, context)
313313
else
314314
error('Unknown binary operation: ' .. tostring(op))
315315
end
316+
elseif node.kind == 'evaluated' then
317+
-- evaluated node can occurs after optimizations
318+
return node.value
316319
else
317320
error('Unknown node kind: ' .. tostring(node.kind))
318321
end
@@ -358,4 +361,6 @@ function expressions.new(str)
358361
})
359362
end
360363

364+
expressions.execute_node = execute_node
365+
361366
return expressions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
local expressions = require('graphql.expressions')
2+
3+
local constant_propagation = {}
4+
5+
local function evaluate_node(node, context)
6+
local value = expressions.execute_node(node, context)
7+
return {
8+
kind = 'evaluated',
9+
value = value,
10+
}
11+
end
12+
13+
--- Fold variables and constant expressions into constants.
14+
---
15+
--- Introdices the new node type:
16+
---
17+
--- {
18+
--- kind = 'evaluated',
19+
--- value = <...> (of any type),
20+
--- }
21+
---
22+
--- @tparam table current AST node (e.g. root node)
23+
---
24+
--- @tparam table context table of the following values:
25+
---
26+
--- * variables (table)
27+
---
28+
--- @treturn table transformed AST
29+
local function propagate_constants_internal(node, context)
30+
if node.kind == 'const' or node.kind == 'variable' then
31+
return evaluate_node(node, context)
32+
elseif node.kind == 'object_field' then
33+
return node
34+
elseif node.kind == 'func' then
35+
local new_args = {}
36+
local changed = false -- at least one arg was changed
37+
local evaluated = true -- all args were evaluated
38+
for i = 1, #node.args do
39+
local arg = propagate_constants_internal(node.args[i], context)
40+
table.insert(new_args, arg)
41+
if arg ~= node.args[i] then
42+
changed = true
43+
end
44+
if arg.kind ~= 'evaluated' then
45+
evaluated = false
46+
end
47+
end
48+
if not changed then return node end
49+
local new_node = {
50+
kind = 'func',
51+
name = node.name,
52+
args = new_args,
53+
}
54+
if not evaluated then return new_node end
55+
return evaluate_node(new_node, context)
56+
elseif node.kind == 'unary_operation' then
57+
local arg = propagate_constants_internal(node.node, context)
58+
if arg == node.node then return node end
59+
local new_node = {
60+
kind = 'unary_operation',
61+
op = node.op,
62+
node = arg,
63+
}
64+
if arg.kind ~= 'evaluated' then return new_node end
65+
return evaluate_node(new_node, context)
66+
elseif node.kind == 'binary_operation' then
67+
local left = propagate_constants_internal(node.left, context)
68+
local right = propagate_constants_internal(node.right, context)
69+
70+
if left == node.left and right == node.right then return node end
71+
72+
-- handle the case when both args were evaluated
73+
if left.kind == 'evaluated' and right.kind == 'evaluated' then
74+
return evaluate_node({
75+
kind = 'binary_operation',
76+
op = node.op,
77+
left = left,
78+
right = right,
79+
}, context)
80+
end
81+
82+
-- handle '{false,true} {&&,||} X' and 'X {&&,||} {false,true}'
83+
if node.op == '&&' or node.op == '||' then
84+
local e_node -- evaluated node
85+
local o_node -- other node
86+
87+
if left.kind == 'evaluated' then
88+
e_node = left
89+
o_node = right
90+
elseif right.kind == 'evaluated' then
91+
e_node = right
92+
o_node = left
93+
end
94+
95+
if e_node ~= nil then
96+
assert(type(e_node.value) == 'boolean')
97+
if node.op == '&&' and e_node.value then
98+
-- {true && o_node, o_node && true}
99+
return o_node
100+
elseif node.op == '&&' and not e_node.value then
101+
-- {false && o_node, o_node && false}
102+
return {
103+
kind = 'evaluated',
104+
value = false,
105+
}
106+
elseif node.op == '||' and e_node.value then
107+
-- {true || o_node, o_node || true}
108+
return {
109+
kind = 'evaluated',
110+
value = true,
111+
}
112+
elseif node.op == '||' and not e_node.value then
113+
-- {false || o_node, o_node || false}
114+
return o_node
115+
else
116+
assert(false)
117+
end
118+
end
119+
end
120+
121+
return {
122+
kind = 'binary_operation',
123+
op = node.op,
124+
left = left,
125+
right = right,
126+
}
127+
elseif node.kind == 'evaluated' then
128+
return node
129+
else
130+
error('Unknown node kind: ' .. tostring(node.kind))
131+
end
132+
end
133+
134+
--- Fold variables and constant expressions into constants.
135+
---
136+
--- Introdices the new node type:
137+
---
138+
--- {
139+
--- kind = 'evaluated',
140+
--- value = <...> (of any type),
141+
--- }
142+
---
143+
--- @tparam table expression
144+
---
145+
--- @tparam table context table of the following values:
146+
---
147+
--- * variables (table)
148+
---
149+
--- @treturn table transformed expression
150+
function constant_propagation.transform(expr, context)
151+
local new_ast = propagate_constants_internal(expr.ast, context)
152+
if new_ast == expr.ast then return expr end
153+
return setmetatable({
154+
raw = expr.raw,
155+
ast = new_ast,
156+
}, getmetatable(expr))
157+
end
158+
159+
return constant_propagation
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env tarantool
2+
3+
local tap = require('tap')
4+
local fio = require('fio')
5+
local json = require('json')
6+
7+
-- require in-repo version of graphql/ sources despite current working directory
8+
local cur_dir = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
9+
:gsub('/./', '/'):gsub('/+$', ''))
10+
package.path =
11+
cur_dir .. '/../../?/init.lua' .. ';' ..
12+
cur_dir .. '/../../?.lua' .. ';' ..
13+
package.path
14+
15+
local expressions = require('graphql.expressions')
16+
local constant_propagation = require('graphql.expressions.constant_propagation')
17+
local test_utils = require('test.test_utils')
18+
19+
local cases = {
20+
{
21+
expr = 'x > 7 + $v + 1',
22+
variables = {v = 2},
23+
expected_ast = {
24+
kind = 'binary_operation',
25+
op = '>',
26+
left = {
27+
kind = 'object_field',
28+
path = 'x',
29+
},
30+
right = {
31+
kind = 'evaluated',
32+
value = 10,
33+
},
34+
},
35+
execute = {
36+
{
37+
object = {x = 10},
38+
expected_result = false,
39+
},
40+
{
41+
object = {x = 12},
42+
expected_result = true,
43+
},
44+
}
45+
},
46+
{
47+
expr = 'true && x > 1',
48+
expected_ast = {
49+
kind = 'binary_operation',
50+
op = '>',
51+
left = {
52+
kind = 'object_field',
53+
path = 'x',
54+
},
55+
right = {
56+
kind = 'evaluated',
57+
value = 1,
58+
},
59+
},
60+
execute = {
61+
{
62+
object = {x = 1},
63+
expected_result = false,
64+
},
65+
{
66+
object = {x = 2},
67+
expected_result = true,
68+
},
69+
}
70+
},
71+
{
72+
expr = 'false && x > 1',
73+
expected_ast = {
74+
kind = 'evaluated',
75+
value = false,
76+
},
77+
execute = {
78+
{
79+
object = {x = 1},
80+
expected_result = false,
81+
},
82+
{
83+
object = {x = 2},
84+
expected_result = false,
85+
},
86+
}
87+
},
88+
{
89+
expr = '1 + 2 * $v == -1',
90+
variables = {v = -1},
91+
expected_ast = {
92+
kind = 'evaluated',
93+
value = true,
94+
},
95+
execute = {
96+
{
97+
expected_result = true,
98+
},
99+
}
100+
},
101+
}
102+
103+
local function run_case(test, case)
104+
test:test(case.name or case.expr, function(test)
105+
local plan = 1
106+
if case.expected_ast then
107+
plan = plan + 1
108+
end
109+
plan = plan + #(case.execute or {})
110+
test:plan(plan)
111+
112+
local compiled_expr = expressions.new(case.expr)
113+
local context = {variables = case.variables}
114+
local optimized_expr = constant_propagation.transform(compiled_expr,
115+
context)
116+
if case.expected_ast then
117+
test:is_deeply(optimized_expr.ast, case.expected_ast,
118+
'optimized ast')
119+
end
120+
for _, e in ipairs(case.execute or {}) do
121+
local result = optimized_expr:execute(e.object)
122+
test:is(result, e.expected_result, 'execute with ' ..
123+
json.encode(e.object))
124+
end
125+
local optimized_expr_2 = constant_propagation.transform(optimized_expr,
126+
context)
127+
test:ok(optimized_expr == optimized_expr_2, 'self-applicability')
128+
end)
129+
end
130+
131+
local test = tap.test('constant_propagation')
132+
test:plan(#cases)
133+
134+
for _, case in ipairs(cases) do
135+
test_utils.show_trace(run_case, test, case)
136+
end
137+
138+
os.exit(test:check() == true and 0 or 1)

0 commit comments

Comments
 (0)