Skip to content

Commit f60ff8e

Browse files
Add an optimizer to the E2 compiler
The optimizer performs a constant propagation pass, which doess transformations like: (add (literal 1) (literal 2)) → (literal 3) As a result, there's now no performance penalty for defining variables like: SecondsInDay = 60 * 60 * 24 It also does some peephole optimization, which can simplify expressions according to mathematical identities. It can also remove branches of if statements with constant conditions: if (SecondsInDay < 30) { ... } → #[ nothing ]# There's still a lot of room for further optimization: redundant loads/stores could be dropped, constant subexpressions could be eliminated, and many other techniques.
1 parent c98decd commit f60ff8e

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
--[[
2+
An optimizer for E2 abstract syntax trees, as produced by the parser and
3+
consumed by the compiler.
4+
5+
Currently it only performs some simple peephole optimizations and constant
6+
propagation. Ideally, we'd do type inference as much as possible before
7+
optimizing, which would give us more useful information throughout.
8+
--]]
9+
10+
E2Lib.Optimizer = {}
11+
local Optimizer = E2Lib.Optimizer
12+
Optimizer.__index = Optimizer
13+
14+
local optimizerDebug = CreateConVar("wire_expression2_optimizer_debug", 0,
15+
"Print an E2's abstract syntax tree after optimization"
16+
)
17+
18+
function Optimizer.Execute(root)
19+
local ok, result = xpcall(Optimizer.Process, E2Lib.errorHandler, root)
20+
if ok and optimizerDebug:GetBool() then
21+
print(E2Lib.Parser.DumpTree(result))
22+
end
23+
return ok, result
24+
end
25+
26+
Optimizer.Passes = {}
27+
28+
function Optimizer.Process(tree)
29+
for i = 3, #tree do
30+
local child = tree[i]
31+
if type(child) == "table" and child.__instruction then
32+
tree[i] = Optimizer.Process(child)
33+
end
34+
end
35+
for _, pass in ipairs(Optimizer.Passes) do
36+
local action = pass[tree[1]]
37+
if action then
38+
tree = assert(action(tree))
39+
end
40+
end
41+
tree.__instruction = true
42+
return tree
43+
end
44+
45+
local constantPropagation = {}
46+
47+
local function evaluateBinary(instruction)
48+
-- this is a little sneaky: we use the operators previously registered with getOperator
49+
-- to do compile-time evaluation, even though it really wasn't designed for it.
50+
local op = wire_expression2_funcs["op:" .. instruction[1] .. "(" .. instruction[3][4] .. instruction[4][4] .. ")"]
51+
local x, y = instruction[3][3], instruction[4][3]
52+
53+
local value = op[3](nil, {nil, {function() return x end}, {function() return y end}})
54+
local type = op[2]
55+
return {"literal", instruction[2], value, type}
56+
end
57+
58+
local function evaluateUnary(instruction)
59+
local op = wire_expression2_funcs["op:" .. instruction[1] .. "(" .. instruction[3][4] .. ")"]
60+
local x = instruction[3][3]
61+
62+
local value = op[3](nil, {nil, {function() return x end}})
63+
local type = op[2]
64+
return {"literal", instruction[2], value, type}
65+
end
66+
67+
for _, operator in pairs({ "add", "sub", "mul", "div", "mod", "exp", "eq", "neq", "geq", "leq",
68+
"gth", "lth", "band", "band", "bor", "bxor", "bshl", "bshr" }) do
69+
constantPropagation[operator] = function(instruction)
70+
if instruction[3][1] ~= "literal" or instruction[4][1] ~= "literal" then return instruction end
71+
return evaluateBinary(instruction)
72+
end
73+
end
74+
75+
function constantPropagation.neg(instruction)
76+
if instruction[3][1] ~= "literal" then return instruction end
77+
return evaluateUnary(instruction)
78+
end
79+
80+
constantPropagation["not"] = function(instruction)
81+
if instruction[3][1] ~= "literal" then return instruction end
82+
instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]})
83+
return evaluateUnary(instruction)
84+
end
85+
86+
for _, operator in pairs({ "and", "or" }) do
87+
constantPropagation[operator] = function(instruction)
88+
if instruction[3][1] ~= "literal" then return instruction end
89+
instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]})
90+
instruction[4] = evaluateUnary({"is", instruction[2], instruction[4]})
91+
return evaluateBinary(instruction)
92+
end
93+
end
94+
95+
table.insert(Optimizer.Passes, constantPropagation)
96+
97+
98+
local peephole = {}
99+
function peephole.add(instruction)
100+
-- (add 0 x) → x
101+
if instruction[3][1] == "literal" and instruction[3][3] == 0 then return instruction[4] end
102+
-- (add x 0) → x
103+
if instruction[4][1] == "literal" and instruction[4][3] == 0 then return instruction[3] end
104+
-- (add (neg x) (neg y)) → (neg (add x y))
105+
if instruction[3][1] == "neg" and instruction[4][1] == "neg" then
106+
return {"neg", instruction[2], {"add", instruction[2], instruction[3][3], instruction[4][3],
107+
__instruction = true}}
108+
end
109+
-- (add x (neg y)) → (sub x y)
110+
if instruction[4][1] == "neg" then
111+
return {"sub", instruction[2], instruction[3], instruction[4][3]}
112+
end
113+
-- (add (neg x) y) → (sub y x)
114+
if instruction[3][1] == "neg" then
115+
return {"sub", instruction[2], instruction[4], instruction[3][3]}
116+
end
117+
return instruction
118+
end
119+
120+
function peephole.sub(instruction)
121+
-- (sub 0 x) → (neg x)
122+
if instruction[3][1] == "literal" and instruction[3][3] == 0 then
123+
return {"neg", instruction[2], instruction[4]}
124+
end
125+
-- (sub x 0) → x
126+
if instruction[4][1] == "literal" and instruction[4][3] == 0 then return instruction[3] end
127+
-- (sub (neg x) (neg y)) → (sub y x)
128+
if instruction[3][1] == "neg" and instruction[4][1] == "neg" then
129+
return {"sub", instruction[2], instruction[4][3], instruction[3][3]}
130+
end
131+
-- (sub x (neg y) → (add x y))
132+
if instruction[4][1] == "neg" then
133+
return {"add", instruction[2], instruction[3], instruction[4][3]}
134+
end
135+
-- (sub (neg x) y) → (neg (add x y))
136+
if instruction[3][1] == "neg" then
137+
return {"neg", instruction[2], {"add", instruction[2], instruction[3][3], instruction[4],
138+
__instruction = true }}
139+
end
140+
return instruction
141+
end
142+
143+
function peephole.mul(instruction)
144+
if instruction[4][1] == "literal" and instruction[3][1] ~= "literal" then
145+
instruction[3], instruction[4] = instruction[4], instruction[3]
146+
end
147+
-- (mul 1 x) → x
148+
if instruction[3][1] == "literal" and instruction[3][3] == 1 then return instruction[4] end
149+
-- (mul 0 x) → 0
150+
if instruction[3][1] == "literal" and instruction[3][3] == 0 then return instruction[3] end
151+
-- (mul -1 x) → (neg x)
152+
if instruction[3][1] == "literal" and instruction[3][3] == -1 then
153+
return {"neg", instruction[2], instruction[4]}
154+
end
155+
return instruction
156+
end
157+
158+
function peephole.neg(instruction)
159+
-- (neg (neg x)) → x
160+
if instruction[3][1] == "neg" then return instruction[3][3] end
161+
return instruction
162+
end
163+
164+
peephole["if"] = function(instruction)
165+
-- (if 1 x y) → x
166+
-- (if 0 x y) → y
167+
if instruction[3][1] == "literal" then
168+
instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]})
169+
if instruction[3][3] == 1 then return instruction[4] end
170+
if instruction[3][3] == 0 then return instruction[5] end
171+
assert(false, "unreachable: `is` evaluation didn't return a boolean")
172+
end
173+
return instruction
174+
end
175+
176+
function peephole.whl(instruction)
177+
-- (while 0 x) → (seq)
178+
if instruction[3][1] == "literal" then
179+
instruction[3] = evaluateUnary({"is", instruction[2], instruction[3]})
180+
if instruction[3][3] == 0 then return {"seq", instruction[2]} end
181+
end
182+
return instruction
183+
end
184+
185+
table.insert(Optimizer.Passes, peephole)

lua/entities/gmod_wire_expression2/init.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ function ENT:CompileCode(buffer, files, filepath)
257257

258258
if not self:PrepareIncludes(files) then return end
259259

260+
status,tree = E2Lib.Optimizer.Execute(tree)
261+
if not status then self:Error(tree) return end
262+
260263
local status, script, inst = E2Lib.Compiler.Execute(tree, self.inports[3], self.outports[3], self.persists[3], dvars, self.includes)
261264
if not status then self:Error(script) return end
262265

@@ -303,6 +306,12 @@ function ENT:PrepareIncludes(files)
303306
return
304307
end
305308

309+
status, tree = E2Lib.Optimizer.Execute(tree)
310+
if not status then
311+
self:Error("(" .. file .. ")" .. tree)
312+
return
313+
end
314+
306315
self.includes[file] = { tree }
307316
end
308317

lua/entities/gmod_wire_expression2/shared.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ include("core/e2lib.lua")
1717
include("base/preprocessor.lua")
1818
include("base/tokenizer.lua")
1919
include("base/parser.lua")
20+
include("base/optimizer.lua")
2021
include("base/compiler.lua")
2122
include('core/init.lua')

0 commit comments

Comments
 (0)