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.
- data/.gitignore +1 -0
- data/Gemfile +3 -1
- data/bin/furnace-avm2 +27 -13
- data/bin/furnace-avm2-decompiler +14 -4
- data/bin/furnace-avm2-shell +4 -0
- data/furnace-avm2.gemspec +1 -1
- data/lib/furnace-avm2.rb +1 -1
- data/lib/furnace-avm2/abc.rb +3 -0
- data/lib/furnace-avm2/abc/metadata/exception_info.rb +7 -2
- data/lib/furnace-avm2/abc/metadata/method_body_info.rb +4 -2
- data/lib/furnace-avm2/abc/metadata/script_info.rb +8 -3
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_add.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_add_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_declocal.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_declocal_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_decrement.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_decrement_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_divide.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_equals.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_greaterequals.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_greaterthan.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_inclocal.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_inclocal_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_increment.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_increment_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_lessequals.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_lessthan.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_modulo.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_multiply.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_multiply_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_negate.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_negate_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_not.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_strictequals.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_subtract.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic/as3_subtract_i.rb +2 -1
- data/lib/furnace-avm2/abc/opcodes/arithmetic_opcode.rb +5 -0
- data/lib/furnace-avm2/abc/opcodes/function_return/as3_returnvalue.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/function_return/as3_returnvoid.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/function_return_opcode.rb +4 -0
- data/lib/furnace-avm2/abc/opcodes/opcode.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce.rb +1 -1
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_a.rb +4 -1
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_b.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_coerce_s.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_d.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_i.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_o.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_s.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion/as3_convert_u.rb +3 -2
- data/lib/furnace-avm2/abc/opcodes/type_conversion_opcode.rb +10 -0
- data/lib/furnace-avm2/abc/primitives/opcode_sequence.rb +4 -0
- data/lib/furnace-avm2/abc/primitives/record.rb +21 -1
- data/lib/furnace-avm2/source/declaration_tokens/package_token.rb +16 -8
- data/lib/furnace-avm2/source/declaration_tokens/script_token.rb +7 -0
- data/lib/furnace-avm2/source/declaration_tokens/slot_token.rb +12 -4
- data/lib/furnace-avm2/source/declaration_tokens/token_with_traits.rb +10 -9
- data/lib/furnace-avm2/source/decompiler.rb +363 -166
- data/lib/furnace-avm2/source/implementation_tokens/case_token.rb +20 -0
- data/lib/furnace-avm2/source/implementation_tokens/catch_filter_token.rb +7 -0
- data/lib/furnace-avm2/source/implementation_tokens/catch_token.rb +9 -0
- data/lib/furnace-avm2/source/implementation_tokens/closure_token.rb +2 -0
- data/lib/furnace-avm2/source/implementation_tokens/control_flow_token.rb +0 -1
- data/lib/furnace-avm2/source/implementation_tokens/do_while_token.rb +16 -0
- data/lib/furnace-avm2/source/implementation_tokens/else_token.rb +3 -1
- data/lib/furnace-avm2/source/implementation_tokens/finally_token.rb +11 -0
- data/lib/furnace-avm2/source/implementation_tokens/label_name_token.rb +12 -0
- data/lib/furnace-avm2/source/implementation_tokens/this_token.rb +9 -0
- data/lib/furnace-avm2/source/implementation_tokens/try_token.rb +11 -0
- data/lib/furnace-avm2/source/implementation_tokens/unary_operator_token.rb +2 -2
- data/lib/furnace-avm2/source/implementation_tokens/unary_post_operator_token.rb +2 -2
- data/lib/furnace-avm2/transform.rb +2 -0
- data/lib/furnace-avm2/transform/ast_build.rb +234 -92
- data/lib/furnace-avm2/transform/ast_normalize.rb +4 -13
- data/lib/furnace-avm2/transform/cfg_build.rb +74 -33
- data/lib/furnace-avm2/transform/cfg_reduce.rb +457 -75
- data/lib/furnace-avm2/transform/nf_normalize.rb +69 -40
- data/lib/furnace-avm2/transform/propagate_constants.rb +49 -0
- data/lib/furnace-avm2/transform/propagate_labels.rb +31 -0
- data/lib/furnace-avm2/version.rb +1 -1
- data/test/basic.as +111 -3
- data/test/switch.as +27 -0
- metadata +907 -313
- 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
|
-
|
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
|
-
|
4
|
+
def transform(ast, body, finallies)
|
5
|
+
@jumps = Set.new
|
6
|
+
@exceptions = {}
|
5
7
|
|
6
|
-
|
7
|
-
@ast = ast
|
8
|
+
@cfg = CFG::Graph.new
|
8
9
|
|
9
|
-
|
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
|
-
|
16
|
+
unless exc_block = @exceptions[exc.range]
|
17
|
+
exc_block = CFG::Node.new(@cfg, "exc_#{index}")
|
12
18
|
|
13
|
-
|
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
|
-
|
19
|
-
@pending_label
|
20
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
11
|
-
@loop_tails
|
12
|
-
@loop_nonlocal
|
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
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
131
|
+
current_nodes << AST::Node.new(:continue, params)
|
28
132
|
end
|
133
|
+
|
29
134
|
break
|
30
|
-
elsif
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
302
|
+
if block.insns.first == block.cti
|
303
|
+
log nesting, "is a while loop"
|
64
304
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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] =
|
352
|
+
@loop_tails[out_root] = cti_block
|
75
353
|
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
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
|
142
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
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
|
-
#
|
195
|
-
#
|
196
|
-
#
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
568
|
+
node.targets.each do |target|
|
569
|
+
# Skip visited nodes.
|
570
|
+
if visited.include?(target)
|
571
|
+
next
|
572
|
+
end
|
204
573
|
|
205
|
-
|
574
|
+
# Skip back edges.
|
575
|
+
if @loops[target] && @loops[target].include?(node)
|
576
|
+
next
|
577
|
+
end
|
206
578
|
|
207
|
-
|
208
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
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
|