furnace-avm2 0.9.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +3 -1
  3. data/bin/furnace-avm2 +27 -13
  4. data/bin/furnace-avm2-decompiler +14 -4
  5. data/bin/furnace-avm2-shell +4 -0
  6. data/furnace-avm2.gemspec +1 -1
  7. data/lib/furnace-avm2.rb +1 -1
  8. data/lib/furnace-avm2/abc.rb +3 -0
  9. data/lib/furnace-avm2/abc/metadata/exception_info.rb +7 -2
  10. data/lib/furnace-avm2/abc/metadata/method_body_info.rb +4 -2
  11. data/lib/furnace-avm2/abc/metadata/script_info.rb +8 -3
  12. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_add.rb +1 -1
  13. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_add_i.rb +2 -1
  14. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_declocal.rb +1 -1
  15. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_declocal_i.rb +2 -1
  16. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_decrement.rb +1 -1
  17. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_decrement_i.rb +2 -1
  18. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_divide.rb +1 -1
  19. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_equals.rb +1 -1
  20. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_greaterequals.rb +1 -1
  21. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_greaterthan.rb +1 -1
  22. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_inclocal.rb +1 -1
  23. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_inclocal_i.rb +2 -1
  24. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_increment.rb +1 -1
  25. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_increment_i.rb +2 -1
  26. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_lessequals.rb +1 -1
  27. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_lessthan.rb +1 -1
  28. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_modulo.rb +1 -1
  29. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_multiply.rb +1 -1
  30. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_multiply_i.rb +2 -1
  31. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_negate.rb +1 -1
  32. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_negate_i.rb +2 -1
  33. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_not.rb +1 -1
  34. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_strictequals.rb +1 -1
  35. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_subtract.rb +1 -1
  36. data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_subtract_i.rb +2 -1
  37. data/lib/furnace-avm2/abc/opcodes/arithmetic_opcode.rb +5 -0
  38. data/lib/furnace-avm2/abc/opcodes/function_return/as3_returnvalue.rb +1 -1
  39. data/lib/furnace-avm2/abc/opcodes/function_return/as3_returnvoid.rb +1 -1
  40. data/lib/furnace-avm2/abc/opcodes/function_return_opcode.rb +4 -0
  41. data/lib/furnace-avm2/abc/opcodes/opcode.rb +1 -1
  42. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce.rb +1 -1
  43. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_a.rb +4 -1
  44. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_b.rb +3 -2
  45. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_s.rb +3 -2
  46. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_d.rb +3 -2
  47. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_i.rb +3 -2
  48. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_o.rb +3 -2
  49. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_s.rb +3 -2
  50. data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_u.rb +3 -2
  51. data/lib/furnace-avm2/abc/opcodes/type_conversion_opcode.rb +10 -0
  52. data/lib/furnace-avm2/abc/primitives/opcode_sequence.rb +4 -0
  53. data/lib/furnace-avm2/abc/primitives/record.rb +21 -1
  54. data/lib/furnace-avm2/source/declaration_tokens/package_token.rb +16 -8
  55. data/lib/furnace-avm2/source/declaration_tokens/script_token.rb +7 -0
  56. data/lib/furnace-avm2/source/declaration_tokens/slot_token.rb +12 -4
  57. data/lib/furnace-avm2/source/declaration_tokens/token_with_traits.rb +10 -9
  58. data/lib/furnace-avm2/source/decompiler.rb +363 -166
  59. data/lib/furnace-avm2/source/implementation_tokens/case_token.rb +20 -0
  60. data/lib/furnace-avm2/source/implementation_tokens/catch_filter_token.rb +7 -0
  61. data/lib/furnace-avm2/source/implementation_tokens/catch_token.rb +9 -0
  62. data/lib/furnace-avm2/source/implementation_tokens/closure_token.rb +2 -0
  63. data/lib/furnace-avm2/source/implementation_tokens/control_flow_token.rb +0 -1
  64. data/lib/furnace-avm2/source/implementation_tokens/do_while_token.rb +16 -0
  65. data/lib/furnace-avm2/source/implementation_tokens/else_token.rb +3 -1
  66. data/lib/furnace-avm2/source/implementation_tokens/finally_token.rb +11 -0
  67. data/lib/furnace-avm2/source/implementation_tokens/label_name_token.rb +12 -0
  68. data/lib/furnace-avm2/source/implementation_tokens/this_token.rb +9 -0
  69. data/lib/furnace-avm2/source/implementation_tokens/try_token.rb +11 -0
  70. data/lib/furnace-avm2/source/implementation_tokens/unary_operator_token.rb +2 -2
  71. data/lib/furnace-avm2/source/implementation_tokens/unary_post_operator_token.rb +2 -2
  72. data/lib/furnace-avm2/transform.rb +2 -0
  73. data/lib/furnace-avm2/transform/ast_build.rb +234 -92
  74. data/lib/furnace-avm2/transform/ast_normalize.rb +4 -13
  75. data/lib/furnace-avm2/transform/cfg_build.rb +74 -33
  76. data/lib/furnace-avm2/transform/cfg_reduce.rb +457 -75
  77. data/lib/furnace-avm2/transform/nf_normalize.rb +69 -40
  78. data/lib/furnace-avm2/transform/propagate_constants.rb +49 -0
  79. data/lib/furnace-avm2/transform/propagate_labels.rb +31 -0
  80. data/lib/furnace-avm2/version.rb +1 -1
  81. data/test/basic.as +111 -3
  82. data/test/switch.as +27 -0
  83. metadata +907 -313
  84. data/Gemfile.lock +0 -19
@@ -7,10 +7,10 @@ module Furnace::AVM2
7
7
  @options = options
8
8
  end
9
9
 
10
- def transform(ast)
10
+ def transform(ast, *stuff)
11
11
  visit ast
12
12
 
13
- ast
13
+ [ ast, *stuff ]
14
14
  end
15
15
 
16
16
  # (pop x) -> (jump-target) x
@@ -115,21 +115,12 @@ module Furnace::AVM2
115
115
  alias :on_jump_if :fix_boolean
116
116
 
117
117
  def replace_with_nop(node)
118
- if @options[:eliminate_nops]
119
- node.update(:remove)
120
- else
121
- node.update(:nop)
122
- end
118
+ node.update(:nop)
123
119
  end
120
+ alias :on_kill :replace_with_nop
124
121
  alias :on_debug :replace_with_nop
125
122
  alias :on_debug_file :replace_with_nop
126
123
  alias :on_debug_line :replace_with_nop
127
-
128
- def on_nop(node)
129
- if @options[:eliminate_nops]
130
- node.update(:remove)
131
- end
132
- end
133
124
  end
134
125
  end
135
126
  end
@@ -1,32 +1,64 @@
1
1
  module Furnace::AVM2
2
2
  module Transform
3
3
  class CFGBuild
4
- include AST::Visitor
4
+ def transform(ast, body, finallies)
5
+ @jumps = Set.new
6
+ @exceptions = {}
5
7
 
6
- def transform(ast)
7
- @ast = ast
8
+ @cfg = CFG::Graph.new
8
9
 
9
- @jumps = Set.new
10
+ body.exceptions.each_with_index do |exc, index|
11
+ if finallies.find { |f| f[:first_catch] == exc }
12
+ # The first catch in finally is a no-op
13
+ next
14
+ end
10
15
 
11
- visit @ast
16
+ unless exc_block = @exceptions[exc.range]
17
+ exc_block = CFG::Node.new(@cfg, "exc_#{index}")
12
18
 
13
- @cfg = CFG::Graph.new
19
+ dispatch_node = AST::Node.new(:exception_dispatch, [], keep: true)
20
+ exc_block.insns << dispatch_node
21
+ exc_block.cti = dispatch_node
22
+
23
+ @exceptions[exc.range] = exc_block
24
+ end
25
+
26
+ exc_block.target_labels << exc.target_offset
27
+
28
+ if finallies.find { |f| f[:second_catch] == exc }
29
+ exc_block.cti.children <<
30
+ AST::Node.new(:finally,
31
+ [ exc.target_offset ])
32
+ else
33
+ exc_block.cti.children <<
34
+ AST::Node.new(:catch,
35
+ [ (exc.exception.to_astlet if exc.exception),
36
+ exc.variable.to_astlet,
37
+ exc.target_offset ])
38
+ end
39
+ end
14
40
 
15
41
  @pending_label = nil
42
+ @pending_exc_block = nil
43
+ @pending_exc_range = nil
16
44
  @pending_queue = []
17
45
 
18
- @ast.children.each_with_index do |node, index|
19
- @pending_label ||= node.metadata[:label]
20
- @pending_queue << node if ![:nop, :jump].include? node.type
46
+ ast.children.each_with_index do |node, index|
47
+ unless @pending_label
48
+ @pending_label = node.metadata[:label]
49
+
50
+ exception_block_for(@pending_label) do |block, range|
51
+ @pending_exc_block = block
52
+ @pending_exc_range = range
53
+ end
54
+ end
55
+
56
+ @pending_queue << node if ![:nop, :jump, :label].include? node.type
21
57
 
22
- next_node = @ast.children[index + 1]
58
+ next_node = ast.children[index + 1]
23
59
  next_label = next_node.metadata[:label] if next_node
24
60
 
25
61
  case node.type
26
- when :label
27
- @jumps.add node.children.first
28
- node.update :nop
29
-
30
62
  when :return_value, :return_void
31
63
  cutoff(nil, [nil])
32
64
 
@@ -44,7 +76,13 @@ module Furnace::AVM2
44
76
  cutoff(node, jumps_to)
45
77
 
46
78
  else
47
- if @jumps.include? next_label
79
+ *, next_exception_block = exception_block_for(next_label)
80
+
81
+ if @jumps.include?(next_label) || (next_node && next_node.type == :label)
82
+ cutoff(nil, [next_label])
83
+ elsif body.exceptions.find { |ex| ex.target_offset == next_label }
84
+ cutoff(nil, [next_label])
85
+ elsif @pending_exc_block != next_exception_block
48
86
  cutoff(nil, [next_label])
49
87
  end
50
88
  end
@@ -54,33 +92,23 @@ module Furnace::AVM2
54
92
  @cfg.nodes.add exit_node
55
93
  @cfg.exit = exit_node
56
94
 
95
+ @exceptions.values.each do |exc_node|
96
+ @cfg.nodes.add exc_node
97
+ end
98
+
57
99
  @cfg.eliminate_unreachable!
58
100
  @cfg.merge_redundant!
59
101
 
60
102
  @cfg
61
103
  end
62
104
 
63
- # propagate labels
64
- def on_any(node)
65
- return if node == @ast
66
-
67
- label = nil
68
-
69
- node.children.each do |child|
70
- if child.is_a?(AST::Node) && child.metadata[:label]
71
- if label.nil? || child.metadata[:label] < label
72
- label = child.metadata[:label]
73
- end
74
-
75
- child.metadata.delete :label
76
- end
77
- end
78
-
79
- node.metadata[:label] = label if label
80
- end
105
+ private
81
106
 
82
107
  def cutoff(cti, targets)
83
108
  node = CFG::Node.new(@cfg, @pending_label, @pending_queue, cti, targets)
109
+ if @pending_exc_block
110
+ node.exception_label = @pending_exc_block.label
111
+ end
84
112
 
85
113
  if @cfg.nodes.empty?
86
114
  @cfg.entry = node
@@ -89,8 +117,21 @@ module Furnace::AVM2
89
117
  @cfg.nodes.add node
90
118
 
91
119
  @pending_label = nil
120
+ @pending_exc_block = nil
121
+ @pending_exc_range = nil
92
122
  @pending_queue = []
93
123
  end
124
+
125
+ def exception_block_for(label)
126
+ return nil unless label
127
+
128
+ @exceptions.find do |range, block|
129
+ if range.include? label
130
+ yield block, range if block_given?
131
+ true
132
+ end
133
+ end
134
+ end
94
135
  end
95
136
  end
96
137
  end
@@ -1,102 +1,408 @@
1
1
  module Furnace::AVM2
2
2
  module Transform
3
3
  class CFGReduce
4
+ def initialize(options={})
5
+ @verbose = options[:verbose] || false
6
+
7
+ #@verbose = true
8
+ end
9
+
4
10
  def transform(cfg)
5
11
  @cfg = cfg
6
12
 
7
13
  @dom = @cfg.dominators
8
14
  @loops = @cfg.identify_loops
9
15
 
10
- @visited = Set.new
11
- @loop_tails = {}
12
- @loop_nonlocal = Set.new
16
+ @visited = Set.new
17
+ @loop_tails = {}
18
+ @loop_nonlocal = Set.new
19
+
20
+ @postcond_heads = Set.new
21
+ @postcond_tails = Set.new
22
+
23
+ @try_tails = Hash.new { |h,k| h[k] = Set.new }
13
24
 
14
25
  ast, = extended_block(@cfg.entry)
15
26
 
27
+ @visited.add @cfg.exit
28
+ if @visited != @cfg.nodes
29
+ raise "failsafe: not all blocks visited (#{(@cfg.nodes - @visited).map(&:label).join(", ")} left)"
30
+ end
31
+
16
32
  ast
17
33
  end
18
34
 
19
- def extended_block(block, stopgap=nil, loop_stack=[])
35
+ def possibly_wrap_eh(block, nodes, exception, loop_stack, nesting)
36
+ if nodes.any?
37
+ if exception.nil?
38
+ nodes
39
+ else
40
+ log nesting, "exception dispatcher"
41
+
42
+ unless exception.cti.type == :exception_dispatch
43
+ raise "invalid exception cti"
44
+ end
45
+
46
+ @visited.add exception
47
+
48
+ catches = exception.cti.children
49
+ handlers = []
50
+
51
+ root, *tails = find_merge_point([ block ] + exception.targets)
52
+ exception.targets.zip(tails).each_with_index do |(target, tail), index|
53
+ log nesting, "handler #{catches[index].inspect}"
54
+ handler = extended_block(target, tail || root, loop_stack, nesting + 1, nil)
55
+
56
+ node = catches[index]
57
+ if node.type == :catch
58
+ exc_name, var_name = node.children
59
+ handlers.push AST::Node.new(:catch, [
60
+ exc_name, var_name,
61
+ handler
62
+ ], node.metadata)
63
+ elsif node.type == :finally
64
+ handlers.push AST::Node.new(:finally, [
65
+ handler
66
+ ], node.metadata)
67
+ else
68
+ raise "unknown handler type #{node.type}"
69
+ end
70
+ end
71
+
72
+ eh_nodes = [ AST::Node.new(:try, [
73
+ AST::Node.new(:begin, nodes),
74
+ ] + handlers) ]
75
+
76
+ if tails.any? && tails.uniq.count == 1 &&
77
+ @dom[tails.first].include?(exception)
78
+ # Handle a special case whether control doesn't flow after tails
79
+ # of the catches by its own, e.g. the last statement of try
80
+ # block is return.
81
+
82
+ tail_block = tails.first
83
+ elsif @try_tails.has_key? exception
84
+ # Handle a special case where control falls through more than
85
+ # one level of scopes.
86
+ if @try_tails[exception].count > 1
87
+ raise "multiple try block exit points"
88
+ end
89
+
90
+ tail_block = @try_tails[exception].first
91
+ end
92
+
93
+ if tail_block
94
+ tail_code = extended_block(tail_block, nil, loop_stack, nesting + 1, nil)
95
+ eh_nodes.concat tail_code.children
96
+ end
97
+
98
+ eh_nodes
99
+ end
100
+ else
101
+ []
102
+ end
103
+ end
104
+
105
+ def is_loop_head?(block, loop_stack)
106
+ (@loops.include?(block) || @postcond_tails.include?(block)) &&
107
+ loop_stack.include?(block)
108
+ end
109
+
110
+ def is_loop_tail?(block, loop_stack)
111
+ @loop_tails.include?(block) &&
112
+ loop_stack.include?(@loop_tails[block])
113
+ end
114
+
115
+ def extended_block(block, stopgap=nil, loop_stack=[], nesting=0, upper_exc=nil)
20
116
  nodes = []
117
+ prev_block = nil
118
+ current_exception = upper_exc
119
+ current_nodes = []
120
+ exception_changed = false
121
+
122
+ log nesting, "--- STOPGAP: #{stopgap.inspect}"
21
123
 
22
124
  while block
23
- if @loops.include?(block) && loop_stack.include?(block)
24
- # We have just arrived to loop head. Insert `continue'
25
- # and exit.
125
+ log nesting, "BLOCK: #{block.inspect}"
126
+
127
+ if is_loop_head?(block, loop_stack)
128
+ log nesting, "exit: loop head (continue stmt)"
129
+
26
130
  check_nonlocal_loop(loop_stack, block) do |params|
27
- nodes << AST::Node.new(:continue, params)
131
+ current_nodes << AST::Node.new(:continue, params)
28
132
  end
133
+
29
134
  break
30
- elsif @loop_tails.include?(block) &&
31
- loop_stack.include?(@loop_tails[block])
32
- # We have just arrived to loop tail. Insert `break'
33
- # and exit.
135
+ elsif is_loop_tail?(block, loop_stack)
136
+ log nesting, "exit: loop tail (break stmt)"
137
+
34
138
  loop = @loop_tails[block]
35
139
  check_nonlocal_loop(loop_stack, loop) do |params|
36
- nodes << AST::Node.new(:break, params)
140
+ current_nodes << AST::Node.new(:break, params)
37
141
  end
142
+
143
+ break
144
+ elsif loop_stack.first == block && !@loops.include?(block)
145
+ log nesting, "exit: do..while cti block"
38
146
  break
39
147
  elsif block == stopgap
40
- # We have just arrived to a merge point of `if'
41
- # contidional. Exit.
148
+ log nesting, "exit: stopgap encountered"
149
+ break
150
+ elsif block.cti && block.cti.type == :exception_dispatch
151
+ log nesting, "exit: spurious exception dispatch traverse"
42
152
  break
153
+ elsif !upper_exc.nil? && block.exception.nil? && block != @cfg.exit
154
+ log nesting, "exit: leaving try block"
155
+
156
+ @try_tails[upper_exc].add block
157
+
158
+ break
159
+ elsif block == @cfg.exit
160
+ # We have just arrived to exit node.
161
+ break
162
+ end
163
+
164
+ log nesting, "EX: #{(current_exception.label if current_exception) || '-'} " <<
165
+ "NEW-EX: #{(block.exception.label if block.exception) || '-'}"
166
+
167
+ if block.exception != current_exception
168
+ nodes.concat possibly_wrap_eh(prev_block, current_nodes, current_exception, loop_stack, nesting)
169
+
170
+ current_exception = block.exception
171
+ current_nodes = []
172
+ exception_changed = true
43
173
  end
44
174
 
45
175
  if @visited.include? block
46
176
  raise "failsafe: block #{block.label} already visited"
47
- elsif block != @cfg.exit
48
- @visited.add block
49
177
  end
50
178
 
51
- block.insns.each do |insn|
52
- next if insn == block.cti
53
- nodes << insn
54
- end
179
+ prev_block = block
180
+ @visited.add block
55
181
 
56
182
  if block.cti
57
183
  if block.cti.type == :lookup_switch
58
- # this is a switch
184
+ log nesting, "is a switch"
185
+
186
+ # Group cases pointing to the same blocks of code.
187
+ aliases = Hash[block.targets.each_index.
188
+ group_by { |index| block.targets[index] }.values.
189
+ map { |(main, *others)| [ main, others ] }]
190
+
191
+ # Find a merge point for all of the case branches.
192
+ case_branches = block.targets.values_at(*aliases.keys)
193
+ case_merges = find_merge_point(case_branches)
194
+
195
+ # A possible exit point for the statement is a merge which
196
+ # isn't pointed to by a branch. This prediction can fail if
197
+ # there are empty cases.
198
+ possible_exit_points = (case_merges.compact - case_branches).uniq
199
+ if possible_exit_points.count > 1
200
+ raise "multiple possible switch exit points at first guess"
201
+ end
202
+
203
+ exit_point = possible_exit_points.first
204
+ log nesting, "exit point (first guess): #{exit_point.inspect}"
205
+
206
+ # Compute case predecessors for fallthrough.
207
+ case_predecessors = Hash.new { |h,k| h[k] = Set.new }
208
+
209
+ case_branches.zip(case_merges).each do |(branch, merge)|
210
+ if case_branches.include?(merge)
211
+ case_predecessors[merge].add branch
212
+ end
213
+ end
214
+
215
+ # One and only one block may have multiple predecessors.
216
+ # In this case, it is the actual exit point; switch the
217
+ # prediction.
218
+ new_exit_point, = case_predecessors.find { |branch, pred| pred.count > 1 }
219
+ if new_exit_point
220
+ if case_predecessors.find { |branch, pred|
221
+ pred.count > 1 && branch != new_exit_point }
222
+ raise "multiple possible switch exit points at second guess"
223
+ end
224
+
225
+ exit_point = new_exit_point
226
+ log nesting, "exit point (second guess): #{exit_point.inspect}"
227
+ end
228
+
229
+ if exit_point.nil?
230
+ exit_point = stopgap
231
+ log nesting, "exit point (last restort): stopgap #{stopgap.inspect}"
232
+ end
233
+
234
+ # Flatten the one-element sets.
235
+ case_predecessors.each do |branch, pred|
236
+ case_predecessors[branch] = pred.first
237
+ end
238
+
239
+ case_successors = case_predecessors.invert
240
+
241
+ # Generate code for the actual branches. Stopgap is either the
242
+ # another case (fallthrough) or exit point.
243
+ case_bodies = case_branches.zip(case_merges).map do |(branch, merge)|
244
+ if case_branches.include? merge
245
+ branch_stopgap = merge
246
+ else
247
+ branch_stopgap = exit_point
248
+ end
249
+
250
+ extended_block(branch, branch_stopgap, loop_stack, nesting + 1, current_exception)
251
+ end
252
+
253
+ node = AST::Node.new(:begin)
59
254
 
60
- elsif @loops.include?(block)
255
+ # Sort the nodes in the order of fallthrough precedence
256
+ # and assemble the body AST.
257
+ case_pool = case_branches.dup
258
+ while case_pool.any?
259
+ next_branch = case_pool.find { |c| !case_predecessors.has_key?(c) }
260
+ if next_branch.nil?
261
+ raise "circular dependency between cases"
262
+ end
263
+
264
+ body = nil
265
+
266
+ while next_branch
267
+ case_pool.delete next_branch
268
+
269
+ if body && !case_predecessors.has_key?(next_branch)
270
+ body.children << AST::Node.new(:break)
271
+ end
272
+
273
+ main_index = case_branches.index(next_branch)
274
+ body = case_bodies[main_index]
275
+
276
+ [ main_index, *aliases[main_index] ].each do |index|
277
+ if index == 0
278
+ node.children << AST::Node.new(:default)
279
+ else
280
+ node.children << AST::Node.new(:case, [
281
+ AST::Node.new(:integer, [ index - 1 ])
282
+ ])
283
+ end
284
+ end
285
+
286
+ node.children << body
287
+
288
+ next_branch = case_successors[next_branch]
289
+ end
290
+
291
+ body.children << AST::Node.new(:break)
292
+ end
293
+
294
+ current_nodes << AST::Node.new(:switch, [
295
+ block.cti.children.last,
296
+ node
297
+ ])
298
+
299
+ block = exit_point
300
+ elsif @loops.include?(block) && !@postcond_heads.include?(block)
61
301
  # we're trapped in a strange loop
62
- reverse = !block.cti.children[0]
63
- in_root, out_root = block.targets
302
+ if block.insns.first == block.cti
303
+ log nesting, "is a while loop"
64
304
 
65
- # One of the branch targets should reside within
66
- # the loop.
67
- if !@loops[block].include?(in_root)
68
- in_root, out_root = out_root, in_root
69
- reverse = !reverse
305
+ loop_type = :head_cti
306
+ cti_block = block
307
+ else
308
+ back_edges = []
309
+
310
+ @loops[block].each do |loop_block|
311
+ loop_block.targets.each do |target|
312
+ # Find a back edge
313
+ if @dom[loop_block].include? target
314
+ back_edges << loop_block
315
+ end
316
+ end
317
+ end
318
+
319
+ if back_edges.count == 1
320
+ loop_type = :tail_cti
321
+ cti_block = back_edges.first
322
+ else
323
+ raise "invalid back edge count"
324
+ end
325
+ end
326
+
327
+ if loop_type == :infinite
328
+ in_root, out_root = block, nil
329
+
330
+ # out_root = nil is not correct in all cases, i.e.
331
+ # when multiple breaks are present.
332
+
333
+ expr = AST::Node.new(:true)
334
+ else
335
+ reverse = !cti_block.cti.children[0]
336
+ in_root, out_root = cti_block.targets
337
+
338
+ # One of the branch targets should reside within
339
+ # the loop.
340
+ if !@loops[block].include?(in_root)
341
+ in_root, out_root = out_root, in_root
342
+ reverse = !reverse
343
+ end
344
+
345
+ # If we reversed the roots or it was a (jump-if false),
346
+ # then reverse the condition.
347
+ expr = normalize_cti_expr(cti_block, reverse)
70
348
  end
71
349
 
72
350
  # Mark the loop tail so we could detect `break' and
73
351
  # `continue' statements.
74
- @loop_tails[out_root] = block
352
+ @loop_tails[out_root] = cti_block
75
353
 
76
- # If we reversed the roots or it was a (jump-if false),
77
- # then reverse the condition.
78
- expr = normalize_cti_expr(block, reverse)
354
+ # Remove the block from visited set if it is unrelated to the
355
+ # current loop condition, as it should be re-processed.
356
+ if loop_type != :head_cti
357
+ @visited.delete block
358
+
359
+ @postcond_heads.add block
360
+ @postcond_tails.add cti_block
361
+ end
79
362
 
80
- body = extended_block(in_root, nil, [ block, *loop_stack ])
363
+ # Handle a special case: all code in the loop header.
364
+ if loop_type == :tail_cti && cti_block == block
365
+ body = AST::Node.new(:begin)
366
+
367
+ append_instructions(block, body.children)
368
+ else
369
+ body = extended_block(in_root, nil, [ cti_block ] + loop_stack, nesting + 1, current_exception)
370
+ end
81
371
 
82
372
  # [(label name)]
83
373
  # We first parse the body and then add the label before
84
374
  # the loop body if anything in the body requires that label
85
375
  # to be present.
86
376
  if @loop_nonlocal.include?(block)
87
- nodes << AST::Node.new(:label, [ loop_label(block) ])
377
+ current_nodes << AST::Node.new(:label, [ loop_label(block) ])
378
+ end
379
+
380
+ # Map loop types to node types.
381
+ if loop_type == :head_cti || loop_type == :infinite
382
+ loop_node = :while
383
+ else
384
+ loop_node = :do_while
88
385
  end
89
386
 
90
- # (while (condition)
387
+ # (while|do-while (condition)
91
388
  # (body ...))
92
- nodes << AST::Node.new(:while, [
389
+ current_nodes << AST::Node.new(loop_node, [
93
390
  expr,
94
391
  body
95
392
  ])
96
393
 
394
+ # Add cti_block to visited for the do-while case.
395
+ if loop_type == :tail_cti
396
+ @visited.add cti_block
397
+ end
398
+
97
399
  block = out_root
98
400
  else
99
- # this is an `if', `break' or `continue'
401
+ log nesting, "is a conditional"
402
+
403
+ append_instructions(block, current_nodes)
404
+
405
+ # This is an `if'.
100
406
  reverse = !block.cti.children[0]
101
407
  left_root, right_root = block.targets
102
408
 
@@ -110,6 +416,18 @@ module Furnace::AVM2
110
416
  # not have edges coming to it even from other blocks
111
417
  # dominated by this block.
112
418
 
419
+ # A special case: empty if().
420
+ if left_root == right_root
421
+ current_nodes << AST::Node.new(:if, [
422
+ block.cti.children[1],
423
+ AST::Node.new(:begin)
424
+ ])
425
+
426
+ block = left_root
427
+
428
+ next
429
+ end
430
+
113
431
  # If the left root isn't dominated by block,
114
432
  # then it can't be `if' branch.
115
433
  if !completely_dominated?(left_root, block)
@@ -119,8 +437,13 @@ module Furnace::AVM2
119
437
 
120
438
  # If the left root still isn't dominated by block,
121
439
  # then this is not a proper conditional.
122
- unless completely_dominated?(left_root, block)
123
- raise "not-well-formed if"
440
+ # If the left root leads to a loop head or tail, then
441
+ # the code will not be generated for that root, and
442
+ # its dominance is irrelevant.
443
+ if !completely_dominated?(left_root, block) &&
444
+ !(is_loop_head?(left_root, loop_stack) ||
445
+ is_loop_tail?(left_root, loop_stack))
446
+ raise "not well-formed if"
124
447
  end
125
448
 
126
449
  # If the right root is dominated by this block, which
@@ -138,8 +461,11 @@ module Furnace::AVM2
138
461
 
139
462
  # Does this conditional have an `else' block?
140
463
  if completely_dominated?(right_root, block)
141
- # Yes. Find a merge point.
142
- merge = find_merge_point(block, left_root, right_root, loop_stack)
464
+ # Yes. Find merge point.
465
+
466
+ # The function technically finds two merge points,
467
+ # but in case of two heads they're identical.
468
+ merge, = find_merge_point([ left_root, right_root ])
143
469
 
144
470
  # If the merge search did not yield a valid node, use
145
471
  # stopgap for the current block to avoid runaway code
@@ -150,32 +476,42 @@ module Furnace::AVM2
150
476
  # objects contained in arguments of recursive calls. As
151
477
  # the `if's are fully nested when well-formed, we can only
152
478
  # check for collision with innermost stopgap block.
153
- nodes << AST::Node.new(:if, [
154
- expr,
155
- extended_block(left_root, merge || stopgap, loop_stack),
156
- extended_block(right_root, merge || stopgap, loop_stack)
157
- ])
479
+
480
+ log nesting, "left"
481
+ left_code = extended_block(left_root, merge || stopgap, loop_stack, nesting + 1, current_exception)
482
+
483
+ log nesting, "right"
484
+ right_code = extended_block(right_root, merge || stopgap, loop_stack, nesting + 1, current_exception)
485
+
486
+ current_nodes << AST::Node.new(:if, [ expr, left_code, right_code ])
158
487
 
159
488
  block = merge
160
489
  else
161
490
  # No. The "right root" is actually post-if code.
162
- nodes << AST::Node.new(:if, [
163
- expr,
164
- extended_block(left_root, right_root, loop_stack)
165
- ])
491
+
492
+ log nesting, "one-way"
493
+ code = extended_block(left_root, right_root, loop_stack, nesting + 1, current_exception)
494
+
495
+ current_nodes << AST::Node.new(:if, [ expr, code ])
166
496
 
167
497
  block = right_root
168
498
  end
169
499
  end
170
500
  elsif block.targets.count == 1
501
+ append_instructions(block, current_nodes)
502
+
171
503
  block = block.targets.first
172
- elsif block == @cfg.exit
173
- break
174
504
  else
175
505
  raise "invalid target count (#{block.targets.count})"
176
506
  end
177
507
  end
178
508
 
509
+ if exception_changed || nesting == 0
510
+ nodes.concat possibly_wrap_eh(prev_block, current_nodes, current_exception, loop_stack, nesting)
511
+ else
512
+ nodes = current_nodes
513
+ end
514
+
179
515
  AST::Node.new(:begin, nodes)
180
516
  end
181
517
 
@@ -191,34 +527,67 @@ module Furnace::AVM2
191
527
  end
192
528
  end
193
529
 
194
- # A merge point for blocks R (root), L (left) and D (right)
195
- # is first block found with BFS starting at {L,D} so that
196
- # it is dominated by R, but not L or D.
197
- def find_merge_point(root, left, right, loop_stack)
198
- worklist = Set[left, right]
199
- visited = Set[root, left, right]
530
+ # Find a set of merge points for a set of partially diverged
531
+ # paths beginning from `heads'.
532
+ # E.g. here:
533
+ #
534
+ # ----<---
535
+ # / \
536
+ # A -------- B --
537
+ # |\ \
538
+ # | C ---- E - F--- <exit>
539
+ # | /
540
+ # \- D -
541
+ #
542
+ # with A as the root and B, C and E as heads, the function reports
543
+ # following merge points: E for C and D, and nil for B.
544
+ # Note that back edge (denoted by < in the picture) is ignored.
545
+ def find_merge_point(heads)
546
+ seen = Set[]
547
+
548
+ heads.map do |head|
549
+ # Trail is an ordered collection of nodes encountered during
550
+ # BFS. Order of nodes with same rank is irrelevant.
551
+ trail = []
552
+
553
+ worklist = Set[head]
554
+ visited = Set[head]
555
+ while worklist.any?
556
+ node = worklist.first
557
+ worklist.delete node
558
+
559
+ visited.add node
560
+ trail.push node
561
+
562
+ # Nodes which are dominated by the current head aren't relevant
563
+ # for merge point search and will cause false positives.
564
+ unless @dom[node].include? head
565
+ seen.add node
566
+ end
200
567
 
201
- while worklist.any?
202
- node = worklist.first
203
- worklist.delete node
568
+ node.targets.each do |target|
569
+ # Skip visited nodes.
570
+ if visited.include?(target)
571
+ next
572
+ end
204
573
 
205
- visited.add node
574
+ # Skip back edges.
575
+ if @loops[target] && @loops[target].include?(node)
576
+ next
577
+ end
206
578
 
207
- if (@dom[node].include?(root) &&
208
- !(@dom[node].include?(left) ||
209
- @dom[node].include?(right))) ||
210
- loop_stack.include?(node)
211
- return node
579
+ worklist.add target
580
+ end
212
581
  end
213
582
 
214
- node.targets.each do |target|
215
- next if visited.include? target
216
- worklist.add target
583
+ trail
584
+ end.map do |(head, *trail)|
585
+ trail.find do |trail_elem|
586
+ seen.include?(trail_elem)
217
587
  end
588
+ end.map do |tail|
589
+ tail unless tail == @cfg.exit
218
590
  end
219
-
220
- # The paths have diverged.
221
- nil
222
591
  end
223
592
 
224
593
  # Check if the control transfer is nonlocal according to the
@@ -234,6 +603,13 @@ module Furnace::AVM2
234
603
  end
235
604
  end
236
605
 
606
+ def append_instructions(block, nodes)
607
+ block.insns.each do |insn|
608
+ next if insn == block.cti
609
+ nodes << insn
610
+ end
611
+ end
612
+
237
613
  def loop_label(block)
238
614
  "label#{block.label}"
239
615
  end
@@ -245,6 +621,12 @@ module Furnace::AVM2
245
621
  block.cti.children[1]
246
622
  end
247
623
  end
624
+
625
+ private
626
+
627
+ def log(nesting, what)
628
+ $stderr.puts "CFGr: #{" " * nesting}#{what}" if @verbose
629
+ end
248
630
  end
249
631
  end
250
632
  end