Skip to content

Commit c9535ca

Browse files
Merge pull request #1497 from wiremod/e2-optimizer
Add an optimizer for E2 between the parser and compiler
2 parents 63b4b10 + f60ff8e commit c9535ca

File tree

5 files changed

+240
-37
lines changed

5 files changed

+240
-37
lines changed

lua/entities/gmod_wire_expression2/base/compiler.lua

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -648,38 +648,10 @@ function Compiler:InstrIWC(args)
648648
self:Error("Connected operator (->" .. E2Lib.limitString(op, 10) .. ") can only be used on inputs or outputs", args)
649649
end
650650
end
651-
652-
function Compiler:InstrNUM(args)
651+
function Compiler:InstrLITERAL(args)
653652
self.prfcounter = self.prfcounter + 0.5
654-
RunString("E2Lib.Compiler.native = function() return " .. args[3] .. " end")
655-
return { Compiler.native }, "n"
656-
end
657-
658-
function Compiler:InstrNUMI(args)
659-
self.prfcounter = self.prfcounter + 1
660-
Compiler.native = { 0, tonumber(args[3]) }
661-
RunString("local value = E2Lib.Compiler.native E2Lib.Compiler.native = function() return value end")
662-
return { Compiler.native }, "c"
663-
end
664-
665-
function Compiler:InstrNUMJ(args)
666-
self.prfcounter = self.prfcounter + 1
667-
Compiler.native = { 0, 0, tonumber(args[3]), 0 }
668-
RunString("local value = E2Lib.Compiler.native E2Lib.Compiler.native = function() return value end")
669-
return { Compiler.native }, "q"
670-
end
671-
672-
function Compiler:InstrNUMK(args)
673-
self.prfcounter = self.prfcounter + 1
674-
Compiler.native = { 0, 0, 0, tonumber(args[3]) }
675-
RunString("local value = E2Lib.Compiler.native E2Lib.Compiler.native = function() return value end")
676-
return { Compiler.native }, "q"
677-
end
678-
679-
function Compiler:InstrSTR(args)
680-
self.prfcounter = self.prfcounter + 1.0
681-
RunString(string.format("E2Lib.Compiler.native = function() return %q end", args[3]))
682-
return { Compiler.native }, "s"
653+
local value = args[3]
654+
return { function() return value end }, args[4]
683655
end
684656

685657
function Compiler:InstrVAR(args)
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/base/parser.lua

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ E2Lib.Parser = {}
8282
local Parser = E2Lib.Parser
8383
Parser.__index = Parser
8484

85+
local parserDebug = CreateConVar("wire_expression2_parser_debug", 0,
86+
"Print an E2's abstract syntax tree after parsing"
87+
)
88+
8589
function Parser.Execute(...)
8690
-- instantiate Parser
8791
local instance = setmetatable({}, Parser)
@@ -90,6 +94,23 @@ function Parser.Execute(...)
9094
return xpcall(Parser.Process, E2Lib.errorHandler, instance, ...)
9195
end
9296

97+
function Parser.DumpTree(tree, indentation)
98+
indentation = indentation or ''
99+
local str = indentation .. tree[1] .. '(' .. tree[2][1] .. ':' .. tree[2][2] .. ')\n'
100+
indentation = indentation .. ' '
101+
for i = 3, #tree do
102+
local child = tree[i]
103+
if type(child) == 'table' and child.__instruction then
104+
str = str .. Parser.DumpTree(child, indentation)
105+
elseif type(child) == 'string' then
106+
str = str .. indentation .. string.format('%q', child) .. '\n'
107+
else
108+
str = str .. indentation .. tostring(child) .. '\n'
109+
end
110+
end
111+
return str
112+
end
113+
93114
function Parser:Error(message, token)
94115
if token then
95116
error(message .. " at line " .. token[4] .. ", char " .. token[5], 0)
@@ -107,7 +128,9 @@ function Parser:Process(tokens, params)
107128

108129
self:NextToken()
109130
local tree = self:Root()
110-
131+
if parserDebug:GetBool() then
132+
print(Parser.DumpTree(tree))
133+
end
111134
return tree, self.delta, self.includes
112135
end
113136

@@ -127,7 +150,7 @@ end
127150

128151

129152
function Parser:Instruction(trace, name, ...)
130-
return { name, trace, ... }
153+
return { __instruction = true, name, trace, ... }
131154
end
132155

133156

@@ -1337,16 +1360,29 @@ function Parser:Expr17()
13371360
local trace = self:GetTokenTrace()
13381361
local tokendata = self:GetTokenData()
13391362
if isnumber(tokendata) then
1340-
return self:Instruction(trace, "num", tokendata)
1363+
return self:Instruction(trace, "literal", tokendata, "n")
1364+
end
1365+
local num, suffix = tokendata:match("^([-+e0-9.]*)(.*)$")
1366+
num = assert(tonumber(num), "unparseable numeric literal")
1367+
local value, type
1368+
if suffix == "" then
1369+
value, type = num, "n"
1370+
elseif suffix == "i" then
1371+
value, type = {0, num}, "c"
1372+
elseif suffix == "j" then
1373+
value, type = {0, 0, num, 0}, "q"
1374+
elseif suffix == "k" then
1375+
value, type = {0, 0, 0, num}, "q"
1376+
else
1377+
assert(false, "unrecognized numeric suffix " .. suffix)
13411378
end
1342-
local num, tp = tokendata:match("^([-+e0-9.]*)(.*)$")
1343-
return self:Instruction(trace, "num" .. tp, num)
1379+
return self:Instruction(trace, "literal", value, type)
13441380
end
13451381

13461382
if self:AcceptRoamingToken("str") then
13471383
local trace = self:GetTokenTrace()
13481384
local str = self:GetTokenData()
1349-
return self:Instruction(trace, "str", str)
1385+
return self:Instruction(trace, "literal", str, "s")
13501386
end
13511387

13521388
if self:AcceptRoamingToken("trg") then

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)