UNPKG

15.1 kBtext/coffeescriptView Raw
1{all, any, concat, concatMap, difference, foldl, foldl1, union} = require './functional-helpers'
2{beingDeclared, declarationsFor, usedAsExpression, envEnrichments} = require './helpers'
3CS = require './nodes'
4exports = module?.exports ? this
5
6makeDispatcher = (defaultValue, handlers, defaultHandler = (->)) ->
7 handlers_ = {}
8 for [ctors..., handler] in handlers
9 handlers_[ctor::className] = handler for ctor in ctors
10 (node, args...) ->
11 return defaultValue unless node?
12 handler =
13 if Object::hasOwnProperty.call handlers_, node.className
14 handlers_[node.className]
15 else defaultHandler
16 handler.apply node, args
17
18
19isTruthy =
20 makeDispatcher no, [
21 [
22 CS.ArrayInitialiser, CS.Class, CS.DeleteOp, CS.ForIn, CS.ForOf
23 CS.Function, CS.BoundFunction, CS.HeregExp, CS.ObjectInitialiser, CS.Range
24 CS.RegExp, CS.Slice, CS.TypeofOp, CS.While
25 -> yes
26 ]
27 [CS.AssignOp, -> isTruthy @expression]
28 [CS.Block, ->
29 if @statements.length is 0 then no
30 else isTruthy @statements[@statements.length - 1]
31 ]
32 [CS.Bool, CS.Float, CS.Int, CS.String, -> !!@data]
33 [CS.Conditional, ->
34 (isTruthy @condition) and (isTruthy @consequent) or
35 (isFalsey @condition) and isTruthy @alternate
36 ]
37 [CS.LogicalAndOp, -> (isTruthy @left) and isTruthy @right]
38 [CS.LogicalNotOp, -> isFalsey @expression]
39 [CS.LogicalOrOp, -> (isTruthy @left) or isTruthy @right]
40 [CS.Program, -> isTruthy @body]
41 [CS.SeqOp, -> isTruthy @right]
42 [CS.Switch, ->
43 (all @cases, isTruthy) and
44 if @alternate? then isTruthy @alternate else yes
45 ]
46 [CS.SwitchCase, -> isTruthy @consequent]
47 [CS.UnaryExistsOp, ->
48 (isTruthy @expression) or
49 # TODO: comprehensive list of all possibly-falsey and always non-null expressions
50 @expression.instanceof CS.Int, CS.Float, CS.String, CS.UnaryPlusOp, CS.UnaryNegateOp, CS.LogicalNotOp
51 ]
52 ], -> no
53
54isFalsey =
55 makeDispatcher no, [
56 [CS.Null, CS.Undefined, -> yes]
57 [CS.AssignOp, -> isFalsey @expression]
58 [CS.Block, ->
59 if @statements.length is 0 then yes
60 else isFalsey @statements[@statements.length - 1]
61 ]
62 [CS.Bool, CS.Float, CS.Int, CS.String, -> not @data]
63 [CS.Conditional, ->
64 (isTruthy @condition) and (isFalsey @consequent) or
65 (isFalsey @condition) and isFalsey @alternate
66 ]
67 [CS.LogicalAndOp, -> (isFalsey @left) or isFalsey @right]
68 [CS.LogicalNotOp, -> isTruthy @expression]
69 [CS.LogicalOrOp, -> (isFalsey @left) and isFalsey @right]
70 [CS.Program, -> isFalsey @body]
71 [CS.SeqOp, -> isFalsey @right]
72 [CS.Switch, ->
73 (all @cases, isFalsey) and
74 if @alternate? then isFalsey @alternate else yes
75 ]
76 [CS.SwitchCase, -> isFalsey @block]
77 [CS.UnaryExistsOp, -> @expression.instanceof CS.Null, CS.Undefined]
78 ], -> no
79
80mayHaveSideEffects =
81 makeDispatcher no, [
82 [
83 CS.Function, CS.BoundFunction, CS.Null, CS.RegExp, CS.This, CS.Undefined
84 -> no
85 ]
86 [
87 CS.Break, CS.Continue, CS.Debugger, CS.DeleteOp, CS.NewOp, CS.Return, CS.Super
88 CS.PreDecrementOp, CS.PreIncrementOp, CS.PostDecrementOp, CS.PostIncrementOp
89 CS.ClassProtoAssignOp, CS.Constructor, CS.Throw, CS.JavaScript, CS.ExtendsOp
90 -> yes
91 ]
92 [CS.Class, (inScope) ->
93 (mayHaveSideEffects @parent, inScope) or
94 @nameAssignee? and (@name or (beingDeclared @nameAssignee).length > 0)
95 ]
96 [CS.Conditional, (inScope) ->
97 (mayHaveSideEffects @condition, inScope) or
98 (not isFalsey @condition) and (mayHaveSideEffects @consequent, inScope) or
99 (not isTruthy @condition) and mayHaveSideEffects @alternate, inScope
100 ]
101 [CS.DoOp, (inScope) ->
102 return yes unless @expression.instanceof CS.Functions
103 newScope = difference inScope, concatMap @expression.parameters, beingDeclared
104 args = for p in @expression.parameters
105 if p.instanceof CS.AssignOp then p.expression else p
106 return yes if any args, (a) -> mayHaveSideEffects a, newScope
107 mayHaveSideEffects @expression.body, newScope
108 ]
109 [CS.ExistsOp, (inScope) ->
110 return yes if mayHaveSideEffects @left, inScope
111 return no if @left.instanceof CS.Undefined, CS.Null
112 mayHaveSideEffects @right, inScope
113 ]
114 [CS.FunctionApplication, CS.SoakedFunctionApplication, (inScope) ->
115 return yes unless @function.instanceof CS.Function, CS.BoundFunction
116 newScope = difference inScope, concatMap @function.parameters, beingDeclared
117 return yes if any @arguments, (a) -> mayHaveSideEffects a, newScope
118 mayHaveSideEffects @function.body, newScope
119 ]
120 [CS.LogicalAndOp, (inScope) ->
121 return yes if mayHaveSideEffects @left, inScope
122 return no if isFalsey @left
123 mayHaveSideEffects @right, inScope
124 ]
125 [CS.LogicalOrOp, (inScope) ->
126 return yes if mayHaveSideEffects @left, inScope
127 return no if isTruthy @left
128 mayHaveSideEffects @right, inScope
129 ]
130 [CS.While, (inScope) ->
131 (mayHaveSideEffects @condition, inScope) or
132 (not isFalsey @condition) and mayHaveSideEffects @body, inScope
133 ]
134 # category: AssignOp
135 [CS.AssignOp, CS.ClassProtoAssignOp, CS.CompoundAssignOp, (inScope) ->
136 #(mayHaveSideEffects @expression, inScope) or (beingDeclared @assignee).length > 0
137 yes
138 ]
139 # category: Primitive
140 [CS.Bool, CS.Float, CS.Identifier, CS.Int, CS.String, -> no]
141 ], (inScope) ->
142 any @childNodes, (child) =>
143 if child in @listMembers
144 then any @[child], (m) -> mayHaveSideEffects m, inScope
145 else mayHaveSideEffects @[child], inScope
146
147
148
149class exports.Optimiser
150
151 @optimise = => (new this).optimise arguments...
152
153 # expose helpers so people have an easy time writing their own rules
154 @isTruthy = isTruthy
155 @isFalsey = isFalsey
156 @mayHaveSideEffects = mayHaveSideEffects
157
158 defaultRules = [
159
160 # If a program has no side effects, then it is the empty program
161 [CS.Program, ->
162 if @body? and mayHaveSideEffects @body, [] then this
163 else new CS.Program null
164 ]
165
166 # Turn blocks into expressions
167 [CS.Block, ({inScope}) ->
168 switch @statements.length
169 when 0 then (new CS.Undefined).g()
170 when 1 then @statements[0]
171 else
172 foldl @statements[0], @statements[1..], (expr, s) ->
173 new CS.SeqOp expr, s
174 ]
175
176 # Reject unused and inconsequential expressions
177 # TODO: comments
178 [CS.SeqOp, ({inScope, ancestry}) ->
179 canDropLast = not usedAsExpression this, ancestry
180 if mayHaveSideEffects @left, inScope
181 if mayHaveSideEffects @right, inScope then this
182 else if not canDropLast then this
183 else if @right.instanceof CS.Undefined then @left
184 else new CS.SeqOp @left, declarationsFor @right, union inScope, envEnrichments @left, inScope
185 else if (@right.instanceof CS.Identifier) and @right.data is 'eval' and
186 ((ancestry[0]?.instanceof CS.FunctionApplication) and ancestry[0].function is this or
187 (ancestry[0]?.instanceof CS.DoOp) and ancestry[0].expression is this)
188 if (@left.instanceof CS.Int) and 0 <= @left.data <= 9 then this
189 else if mayHaveSideEffects @left, inScope then this
190 else new CS.SeqOp (new CS.Int 0).g(), @right
191 else
192 if mayHaveSideEffects @right, inScope
193 decls = declarationsFor @left, inScope
194 if decls.instanceof CS.Undefined then @right
195 #else new CS.SeqOp decls, @right
196 else this # TODO: I would love to be able to do the above, but it will infinite loop
197 else if canDropLast
198 declarationsFor this, inScope
199 else @right
200 ]
201
202 # TODO: everything after a CS.Return in a CS.SeqOp
203
204 # Push assignments through sequences
205 [CS.AssignOp, ->
206 return this unless @expression.instanceof CS.SeqOp
207 new CS.SeqOp @expression.left, new CS.AssignOp @assignee, @expression.right
208 ]
209
210 # A falsey condition with side effects -> (the condition; [])
211 # A falsey condition without side effects -> []
212 # A truthy condition without side effects -> a loop
213 [CS.While, ({inScope}) ->
214 if isFalsey @condition
215 new CS.Block [
216 if mayHaveSideEffects @condition, inScope
217 new CS.SeqOp @condition, declarationsFor @body
218 else
219 if @body? then declarationsFor @body, inScope else new CS.Undefined
220 new CS.ArrayInitialiser []
221 ]
222 else if isTruthy @condition
223 if mayHaveSideEffects @condition, inScope then this
224 else if @body?
225 if this instanceof CS.Loop then this else (new CS.Loop @body).g()
226 else new CS.ArrayInitialiser []
227 else this
228 ]
229
230 # Produce the consequent when the condition is truthy
231 # Produce the alternative when the condition is falsey
232 # Prepend the condition if it has side effects
233 [CS.Conditional, ({inScope}) ->
234 if isFalsey @condition
235 [removedBlock, block] = [@consequent, @alternate]
236 else if isTruthy @condition
237 [block, removedBlock] = [@consequent, @alternate]
238 else
239 return this
240 decls = declarationsFor removedBlock, inScope
241 block = if block? then new CS.SeqOp decls, block else decls
242 if mayHaveSideEffects @condition, inScope
243 block = new CS.SeqOp @condition, block
244 block
245 ]
246
247 # for-in over an empty list produces an empty list
248 [CS.ForIn, ({inScope}) ->
249 return this unless (@target.instanceof CS.ArrayInitialiser) and @target.members.length is 0
250 new CS.SeqOp (declarationsFor this, inScope), (new CS.ArrayInitialiser []).g()
251 ]
252
253 # for-own-of over empty object produces an empty list
254 [CS.ForOf, ({inScope}) ->
255 return this unless @isOwn and (@target.instanceof CS.ObjectInitialiser) and @target.members.length is 0
256 new CS.SeqOp (declarationsFor this, inScope), (new CS.ArrayInitialiser []).g()
257 ]
258
259 # for-in or for-of with falsey filter
260 [CS.ForIn, CS.ForOf, ({inScope}) ->
261 return this unless isFalsey @filter
262 new CS.SeqOp (declarationsFor this, inScope), (new CS.ArrayInitialiser []).g()
263 ]
264
265 # for-in or for-of with truthy filter
266 [CS.ForIn, ->
267 return this unless isTruthy @filter
268 new CS.ForIn @valAssignee, @keyAssignee, @target, @step, null, @body
269 ]
270 [CS.ForOf, ->
271 return this unless isTruthy @filter
272 new CS.ForOf @isOwn, @keyAssignee, @valAssignee, @target, null, @body
273 ]
274
275 # Arrays in statement position might as well be Seqs
276 [CS.ArrayInitialiser, ({inScope, ancestry}) ->
277 if usedAsExpression this, ancestry then this
278 else
279 foldl (new CS.Undefined).g(), @members, (expr, m) ->
280 new CS.SeqOp expr, m
281 ]
282
283 # Produce the right operand when the left operand is null or undefined
284 [CS.ExistsOp, -> if @left.instanceof CS.Null, CS.Undefined then @right else this]
285
286 # Produce false when the expression is null or undefined
287 [CS.UnaryExistsOp, -> if @expression.instanceof CS.Null, CS.Undefined then (new CS.Bool false).g() else this]
288
289 # LogicalNotOp applied to a literal or !!
290 [CS.LogicalNotOp, ({inScope}) ->
291 switch
292 when @expression.instanceof CS.Int, CS.Float, CS.String, CS.Bool
293 (new CS.Bool !@expression.data).g()
294 when @expression.instanceof CS.Functions then (new CS.Bool false).g()
295 when @expression.instanceof CS.Null, CS.Undefined then (new CS.Bool true).g()
296 when @expression.instanceof CS.ArrayInitialiser, CS.ObjectInitialiser
297 if mayHaveSideEffects @expression, inScope then this
298 else new CS.SeqOp (declarationsFor @expression, inScope), (new CS.Bool false).g()
299 when @expression.instanceof CS.LogicalNotOp
300 if @expression.expression.instanceof CS.LogicalNotOp then @expression.expression
301 else this
302 else this
303 ]
304
305 # typeof on any literal
306 [CS.TypeofOp, ->
307 switch
308 when @expression.instanceof CS.Int, CS.Float, CS.UnaryNegateOp, CS.UnaryPlusOp
309 (new CS.String 'number').g()
310 when @expression.instanceof CS.String then (new CS.String 'string').g()
311 when @expression.instanceof CS.Functions then (new CS.String 'function').g()
312 when @expression.instanceof CS.Undefined then (new CS.String 'undefined').g()
313 # TODO: comprehensive
314 else this
315 ]
316
317 # simplify trailing `return`/`undefined` in function bodies
318 [CS.SeqOp, ({ancestry}) ->
319 return this unless (ancestry[0]?.instanceof CS.Functions) and ancestry[0].body is this
320 if (@right.instanceof CS.Return) and @right.expression?
321 new CS.SeqOp @left, @right.expression
322 else if @right.instanceof CS.Undefined
323 new CS.SeqOp @left, new CS.Return
324 else this
325 ]
326
327 # get rid of function bodies that are simply `return` or `undefined`
328 [CS.Function, CS.BoundFunction, ->
329 return this unless @block? and (
330 (@block.instanceof CS.Undefined) or
331 (@block.instanceof CS.Return) and not @block.expression?
332 )
333 new @constructor @parameters, null
334 ]
335
336 # `return undefined` -> `return`, everywhere
337 [CS.Return, -> if @expression?.instanceof CS.Undefined then new CS.Return else this]
338
339 [CS.Slice, ->
340 if (@left?.instanceof CS.Int, CS.String) and +@left.data is 0
341 new CS.Slice @expression, @isInclusive, null, @right
342 else if @isInclusive and (@right?.instanceof CS.UnaryNegateOp) and (@right.expression.instanceof CS.Int) and @right.expression.data is 1
343 new CS.Slice @expression, yes, @left, null
344 else this
345 ]
346 ]
347
348 constructor: ->
349 @rules = {}
350 for [ctors..., handler] in defaultRules
351 for ctor in ctors
352 @addRule ctor::className, handler
353
354 addRule: (ctor, handler) ->
355 (@rules[ctor] ?= []).push handler
356 this
357
358 optimise: do ->
359
360 walk = (fn, inScope = [], ancestry = []) ->
361 if not this? or this is global
362 throw new Error 'Optimiser rules must produce a node. `null` is not a node.'
363 return this if this in ancestry
364 ancestry.unshift this
365 for childName in @childNodes when @[childName]?
366 if childName in @listMembers
367 for member, n in @[childName]
368 while @[childName][n] isnt walk.call (@[childName][n] = fn.call @[childName][n], {inScope, ancestry}), fn, inScope, ancestry then
369 inScope = union inScope, envEnrichments @[childName][n], inScope
370 else
371 while @[childName] isnt walk.call (@[childName] = fn.call @[childName], {inScope, ancestry}), fn, inScope, ancestry then
372 inScope = union inScope, envEnrichments @[childName], inScope
373 do ancestry.shift
374 jsNode = fn.call this, {inScope, ancestry}
375 jsNode[p] = @[p] for p in ['raw', 'line', 'column', 'offset']
376 jsNode
377
378 (ast) ->
379 rules = @rules
380 walk.call ast, ->
381 # not a fold for efficiency's sake
382 memo = this
383 oldClassName = null
384 while oldClassName isnt memo.className
385 for rule in rules[oldClassName = memo.className] ? []
386 memo = rule.apply memo, arguments
387 break unless oldClassName is memo.className
388 memo