zombie-killer 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +28 -0
- data/bin/zk +45 -0
- data/lib/zombie_killer.rb +4 -0
- data/lib/zombie_killer/code_histogram.rb +50 -0
- data/lib/zombie_killer/killer.rb +35 -0
- data/lib/zombie_killer/niceness.rb +60 -0
- data/lib/zombie_killer/node_type_counter.rb +29 -0
- data/lib/zombie_killer/rewriter.rb +303 -0
- data/lib/zombie_killer/variable_scope.rb +62 -0
- data/lib/zombie_killer/version.rb +3 -0
- data/spec/rspec_renderer.rb +172 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/zombie_killer_spec.md +1101 -0
- data/spec/zombie_killer_spec.rb +1148 -0
- metadata +145 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# Tracks state for a variable
|
2
|
+
class VariableState
|
3
|
+
attr_accessor :nice
|
4
|
+
end
|
5
|
+
|
6
|
+
# Tracks state for local variables visible at certain point.
|
7
|
+
# Keys are symbols, values are VariableState
|
8
|
+
class VariableScope < Hash
|
9
|
+
def initialize
|
10
|
+
super do |hash, key|
|
11
|
+
hash[key] = VariableState.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Deep copy the VariableState values
|
16
|
+
def dup
|
17
|
+
copy = self.class.new
|
18
|
+
each do |k, v|
|
19
|
+
copy[k] = v.dup
|
20
|
+
end
|
21
|
+
copy
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [VariableState] state
|
25
|
+
def [](varname)
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
# Set state for a variable
|
30
|
+
def []=(varname, state)
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# A stack of VariableScope
|
36
|
+
class VariableScopeStack
|
37
|
+
def initialize
|
38
|
+
outer_scope = VariableScope.new
|
39
|
+
@stack = [outer_scope]
|
40
|
+
end
|
41
|
+
|
42
|
+
# The innermost, or current VariableScope
|
43
|
+
def innermost
|
44
|
+
@stack.last
|
45
|
+
end
|
46
|
+
|
47
|
+
# Run *block* using a new clean scope
|
48
|
+
# @return the scope as the block left it, popped from the stack
|
49
|
+
def with_new(&block)
|
50
|
+
@stack.push VariableScope.new
|
51
|
+
block.call
|
52
|
+
@stack.pop
|
53
|
+
end
|
54
|
+
|
55
|
+
# Run *block* using a copy of the innermost scope
|
56
|
+
# @return the scope as the block left it, popped from the stack
|
57
|
+
def with_copy(&block)
|
58
|
+
@stack.push innermost.dup
|
59
|
+
block.call
|
60
|
+
@stack.pop
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require "redcarpet"
|
2
|
+
|
3
|
+
require_relative "../lib/zombie_killer"
|
4
|
+
|
5
|
+
# Utility functions for manipulating code.
|
6
|
+
module Code
|
7
|
+
INDENT_STEP = 2
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def join(lines)
|
11
|
+
lines.map { |l| "#{l}\n" }.join("")
|
12
|
+
end
|
13
|
+
|
14
|
+
def indent(s)
|
15
|
+
s.gsub(/^(?=.)/, " " * INDENT_STEP)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Represents RSpec's "it" block.
|
21
|
+
class It
|
22
|
+
def initialize(attrs)
|
23
|
+
@description = attrs[:description]
|
24
|
+
@code = attrs[:code]
|
25
|
+
@skip = attrs[:skip]
|
26
|
+
end
|
27
|
+
|
28
|
+
def render
|
29
|
+
[
|
30
|
+
"#{@skip ? "xit" : "it"} #{@description.inspect} do",
|
31
|
+
Code.indent(@code),
|
32
|
+
"end"
|
33
|
+
].join("\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Represents RSpec's "describe" block.
|
38
|
+
class Describe
|
39
|
+
attr_reader :blocks
|
40
|
+
|
41
|
+
def initialize(attrs)
|
42
|
+
@description = attrs[:description]
|
43
|
+
@blocks = attrs[:blocks]
|
44
|
+
end
|
45
|
+
|
46
|
+
def render
|
47
|
+
parts = []
|
48
|
+
parts << "describe #{@description.inspect} do"
|
49
|
+
if !blocks.empty?
|
50
|
+
parts << Code.indent(@blocks.map(&:render).join("\n\n"))
|
51
|
+
end
|
52
|
+
parts << "end"
|
53
|
+
parts.join("\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class RSpecRenderer < Redcarpet::Render::Base
|
58
|
+
def initialize
|
59
|
+
super
|
60
|
+
|
61
|
+
@next_block_type = :unknown
|
62
|
+
@describe = Describe.new(description: "ZombieKiller:", blocks: [])
|
63
|
+
end
|
64
|
+
|
65
|
+
def header(text, header_level)
|
66
|
+
return nil if header_level == 1
|
67
|
+
|
68
|
+
if header_level > describes_depth + 1
|
69
|
+
raise "Missing higher level header: #{text}"
|
70
|
+
end
|
71
|
+
|
72
|
+
describe_at_level(header_level - 1).blocks << Describe.new(
|
73
|
+
description: text.downcase + ":",
|
74
|
+
blocks: []
|
75
|
+
)
|
76
|
+
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def paragraph(text)
|
81
|
+
if text =~ /^\*\*(.*)\*\*$/
|
82
|
+
@next_block_type = $1.downcase.to_sym
|
83
|
+
else
|
84
|
+
first_sentence = text.split(/\.(\s+|$)/).first
|
85
|
+
@description = first_sentence.sub(/^Zombie Killer /, "").sub(/\n/, " ")
|
86
|
+
end
|
87
|
+
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def block_code(code, language)
|
92
|
+
case @next_block_type
|
93
|
+
when :original
|
94
|
+
@original_code = code[0..-2]
|
95
|
+
when :translated
|
96
|
+
@translated_code = code[0..-2]
|
97
|
+
when :unchanged
|
98
|
+
@original_code = @translated_code = code[0..-2]
|
99
|
+
else
|
100
|
+
raise "Invalid next code block type: #@next_block_type.\n#{code}"
|
101
|
+
end
|
102
|
+
@next_block_type = :unknown
|
103
|
+
|
104
|
+
if @original_code && @translated_code
|
105
|
+
current_describe.blocks << It.new(
|
106
|
+
description: @description,
|
107
|
+
code: generate_test_code,
|
108
|
+
skip: @description =~ /XFAIL/
|
109
|
+
)
|
110
|
+
|
111
|
+
@original_code = nil
|
112
|
+
@translated_code = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def doc_header
|
119
|
+
Code.join([
|
120
|
+
"# Generated from spec/zombie_killer_spec.md -- do not change!",
|
121
|
+
"",
|
122
|
+
"require \"spec_helper\"",
|
123
|
+
"",
|
124
|
+
])
|
125
|
+
end
|
126
|
+
|
127
|
+
def doc_footer
|
128
|
+
"#{@describe.render}\n"
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def describes_depth
|
134
|
+
describe = @describe
|
135
|
+
depth = 1
|
136
|
+
while describe.blocks.last.is_a?(Describe)
|
137
|
+
describe = describe.blocks.last
|
138
|
+
depth += 1
|
139
|
+
end
|
140
|
+
depth
|
141
|
+
end
|
142
|
+
|
143
|
+
def current_describe
|
144
|
+
describe = @describe
|
145
|
+
while describe.blocks.last.is_a?(Describe)
|
146
|
+
describe = describe.blocks.last
|
147
|
+
end
|
148
|
+
describe
|
149
|
+
end
|
150
|
+
|
151
|
+
def describe_at_level(level)
|
152
|
+
describe = @describe
|
153
|
+
2.upto(level) do
|
154
|
+
describe = describe.blocks.last
|
155
|
+
end
|
156
|
+
describe
|
157
|
+
end
|
158
|
+
|
159
|
+
def generate_test_code
|
160
|
+
[
|
161
|
+
"original_code = cleanup(<" + "<-EOT)", # splitting un-confuses Emacs
|
162
|
+
Code.indent(@original_code),
|
163
|
+
"EOT",
|
164
|
+
"",
|
165
|
+
"translated_code = cleanup(<" + "<-EOT)", # splitting un-confuses Emacs
|
166
|
+
Code.indent(@translated_code),
|
167
|
+
"EOT",
|
168
|
+
"",
|
169
|
+
"expect(ZombieKiller.new.kill(original_code)).to eq(translated_code)"
|
170
|
+
].join("\n")
|
171
|
+
end
|
172
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,1101 @@
|
|
1
|
+
Zombie Killer Specification
|
2
|
+
===========================
|
3
|
+
|
4
|
+
This document describes how [Zombie
|
5
|
+
Killer](https://github.com/yast/zombie-killer) kills various YCP zombies. It
|
6
|
+
serves both as a human-readable documentation and as an executable
|
7
|
+
specification. Technically, this is implemented by translating this document
|
8
|
+
from [Markdown](http://daringfireball.net/projects/markdown/) into
|
9
|
+
[RSpec](http://rspec.info/).
|
10
|
+
|
11
|
+
Table Of Contents
|
12
|
+
-----------------
|
13
|
+
|
14
|
+
1. Concepts
|
15
|
+
1. Literals
|
16
|
+
1. Variables
|
17
|
+
1. Assigments
|
18
|
+
1. And-assignment
|
19
|
+
1. Or-assignment
|
20
|
+
1. Calls Preserving Niceness
|
21
|
+
1. Calls Generating Niceness
|
22
|
+
1. Translation Below Top Level
|
23
|
+
1. Chained Translation
|
24
|
+
1. If
|
25
|
+
1. Case
|
26
|
+
1. Loops
|
27
|
+
1. While and Until
|
28
|
+
1. For
|
29
|
+
1. Exceptions
|
30
|
+
1. Blocks
|
31
|
+
1. Formatting
|
32
|
+
|
33
|
+
Concepts
|
34
|
+
--------
|
35
|
+
|
36
|
+
A **zombie** is a Ruby method call emulating a quirk of the YCP language that
|
37
|
+
YaST was formerly implemented in. `Ops.add` will serve as an example of a
|
38
|
+
simple zombie. The library implementation simply returns `nil` if any argument
|
39
|
+
is `nil`. Compare this to `+` which raises an exception if it gets
|
40
|
+
`nil`. Therefore `Ops.add` can be translated to the `+` operator, as long as
|
41
|
+
its arguments are not `nil`.
|
42
|
+
|
43
|
+
A **nice** value is one that cannot be `nil` and is therefore suitable as an
|
44
|
+
argument to a native operator.
|
45
|
+
|
46
|
+
An **ugly** value is one that may be `nil`.
|
47
|
+
|
48
|
+
Literals
|
49
|
+
--------
|
50
|
+
|
51
|
+
String and integer literals are obviously nice. `nil` is a literal too but it
|
52
|
+
is ugly.
|
53
|
+
|
54
|
+
Zombie Killer translates `Ops.add` of two string literals.
|
55
|
+
|
56
|
+
**Original**
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Ops.add("Hello", "World")
|
60
|
+
```
|
61
|
+
|
62
|
+
**Translated**
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
"Hello" + "World"
|
66
|
+
```
|
67
|
+
|
68
|
+
Zombie Killer translates `Ops.add` of two integer literals.
|
69
|
+
|
70
|
+
**Original**
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Ops.add(40, 2)
|
74
|
+
```
|
75
|
+
|
76
|
+
**Translated**
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
40 + 2
|
80
|
+
```
|
81
|
+
|
82
|
+
Zombie Killer translates assignment of `Ops.add` of two string literals.
|
83
|
+
(Move this to "translate deeper than at top level")
|
84
|
+
|
85
|
+
**Original**
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
v = Ops.add("Hello", "World")
|
89
|
+
```
|
90
|
+
|
91
|
+
**Translated**
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
v = "Hello" + "World"
|
95
|
+
```
|
96
|
+
|
97
|
+
Zombie Killer does not translate Ops.add if any argument is ugly.
|
98
|
+
|
99
|
+
**Unchanged**
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
Ops.add("Hello", world)
|
103
|
+
```
|
104
|
+
|
105
|
+
Zombie Killer does not translate Ops.add if any argument is the nil literal.
|
106
|
+
|
107
|
+
**Unchanged**
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
Ops.add("Hello", nil)
|
111
|
+
```
|
112
|
+
|
113
|
+
Variables
|
114
|
+
---------
|
115
|
+
|
116
|
+
If a local variable is assigned a nice value, we remember that.
|
117
|
+
|
118
|
+
Zombie Killer translates `Ops.add(nice_variable, literal)`.
|
119
|
+
|
120
|
+
**Original**
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
v = "Hello"
|
124
|
+
Ops.add(v, "World")
|
125
|
+
```
|
126
|
+
|
127
|
+
**Translated**
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
v = "Hello"
|
131
|
+
v + "World"
|
132
|
+
```
|
133
|
+
|
134
|
+
Zombie Killer doesn't translate `Ops.add(nice_variable, literal)` when the
|
135
|
+
variable got it's niceness via multiple assignemnt. We chose to ignore multiple
|
136
|
+
assigments for now because of their complicated semantics (especially in
|
137
|
+
presence of splats).
|
138
|
+
|
139
|
+
**Unchanged**
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
v1, v2 = "Hello", "World"
|
143
|
+
Ops.add(v1, v2)
|
144
|
+
```
|
145
|
+
|
146
|
+
Zombie Killer translates `Ops.add(nontrivially_nice_variable, literal)`.
|
147
|
+
|
148
|
+
**Original**
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
v = "Hello"
|
152
|
+
v2 = v
|
153
|
+
v = uglify
|
154
|
+
Ops.add(v2, "World")
|
155
|
+
```
|
156
|
+
|
157
|
+
**Translated**
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
v = "Hello"
|
161
|
+
v2 = v
|
162
|
+
v = uglify
|
163
|
+
v2 + "World"
|
164
|
+
```
|
165
|
+
|
166
|
+
We have to take care to revoke a variable's niceness if appropriate.
|
167
|
+
|
168
|
+
Zombie Killer does not translate `Ops.add(mutated_variable, literal)`.
|
169
|
+
|
170
|
+
**Unchanged**
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
v = "Hello"
|
174
|
+
v = f(v)
|
175
|
+
Ops.add(v, "World")
|
176
|
+
```
|
177
|
+
|
178
|
+
Zombie Killer does not confuse variables across `def`s.
|
179
|
+
|
180
|
+
**Unchanged**
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
def a
|
184
|
+
v = "literal"
|
185
|
+
end
|
186
|
+
|
187
|
+
def b(v)
|
188
|
+
Ops.add(v, "literal")
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
Zombie Killer does not confuse variables across `def self.`s.
|
193
|
+
|
194
|
+
**Unchanged**
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
v = 1
|
198
|
+
|
199
|
+
def self.foo(v)
|
200
|
+
Ops.add(v, 1)
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
Zombie Killer does not confuse variables across `module`s.
|
205
|
+
|
206
|
+
**Unchanged**
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
module A
|
210
|
+
v = "literal"
|
211
|
+
end
|
212
|
+
|
213
|
+
module B
|
214
|
+
# The assignment is needed to convince Ruby parser that the "v" reference in
|
215
|
+
# the "Ops.add" call later refers to a variable, not a method. This means it
|
216
|
+
# will be parsed as a "lvar" node (which can possibly be nice), not a "send"
|
217
|
+
# node (which can't be nice).
|
218
|
+
|
219
|
+
v = v
|
220
|
+
Ops.add(v, "literal")
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
Zombie Killer does not confuse variables across `class`s.
|
225
|
+
|
226
|
+
**Unchanged**
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class A
|
230
|
+
v = "literal"
|
231
|
+
end
|
232
|
+
|
233
|
+
class B
|
234
|
+
# The assignment is needed to convince Ruby parser that the "v" reference in
|
235
|
+
# the "Ops.add" call later refers to a variable, not a method. This means it
|
236
|
+
# will be parsed as a "lvar" node (which can possibly be nice), not a "send"
|
237
|
+
# node (which can't be nice).
|
238
|
+
|
239
|
+
v = v
|
240
|
+
Ops.add(v, "literal")
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
Zombie Killer does not confuse variables across singleton `class`s.
|
245
|
+
|
246
|
+
**Unchanged**
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class << self
|
250
|
+
v = "literal"
|
251
|
+
end
|
252
|
+
|
253
|
+
class << self
|
254
|
+
# The assignment is needed to convince Ruby parser that the "v" reference in
|
255
|
+
# the "Ops.add" call later refers to a variable, not a method. This means it
|
256
|
+
# will be parsed as a "lvar" node (which can possibly be nice), not a "send"
|
257
|
+
# node (which can't be nice).
|
258
|
+
|
259
|
+
v = v
|
260
|
+
Ops.add(v, "literal")
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
Assignments
|
265
|
+
-----------
|
266
|
+
|
267
|
+
### And-assignment
|
268
|
+
|
269
|
+
Zombie Killer manages niceness correctly in presence of `&&=`.
|
270
|
+
|
271
|
+
**Original**
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
nice1 = true
|
275
|
+
nice2 = true
|
276
|
+
ugly1 = nil
|
277
|
+
ugly2 = nil
|
278
|
+
|
279
|
+
nice1 &&= true
|
280
|
+
nice2 &&= nil
|
281
|
+
ugly1 &&= true
|
282
|
+
ugly2 &&= nil
|
283
|
+
|
284
|
+
Ops.add(nice1, 1)
|
285
|
+
Ops.add(nice2, 1)
|
286
|
+
Ops.add(ugly1, 1)
|
287
|
+
Ops.add(ugly2, 1)
|
288
|
+
```
|
289
|
+
|
290
|
+
**Translated**
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
nice1 = true
|
294
|
+
nice2 = true
|
295
|
+
ugly1 = nil
|
296
|
+
ugly2 = nil
|
297
|
+
|
298
|
+
nice1 &&= true
|
299
|
+
nice2 &&= nil
|
300
|
+
ugly1 &&= true
|
301
|
+
ugly2 &&= nil
|
302
|
+
|
303
|
+
nice1 + 1
|
304
|
+
Ops.add(nice2, 1)
|
305
|
+
Ops.add(ugly1, 1)
|
306
|
+
Ops.add(ugly2, 1)
|
307
|
+
```
|
308
|
+
|
309
|
+
### Or-assignment
|
310
|
+
|
311
|
+
Zombie Killer manages niceness correctly in presence of `||=`.
|
312
|
+
|
313
|
+
**Original**
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
nice1 = true
|
317
|
+
nice2 = true
|
318
|
+
ugly1 = nil
|
319
|
+
ugly2 = nil
|
320
|
+
|
321
|
+
nice1 ||= true
|
322
|
+
nice2 ||= nil
|
323
|
+
ugly1 ||= true
|
324
|
+
ugly2 ||= nil
|
325
|
+
|
326
|
+
Ops.add(nice1, 1)
|
327
|
+
Ops.add(nice2, 1)
|
328
|
+
Ops.add(ugly1, 1)
|
329
|
+
Ops.add(ugly2, 1)
|
330
|
+
```
|
331
|
+
|
332
|
+
**Translated**
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
nice1 = true
|
336
|
+
nice2 = true
|
337
|
+
ugly1 = nil
|
338
|
+
ugly2 = nil
|
339
|
+
|
340
|
+
nice1 ||= true
|
341
|
+
nice2 ||= nil
|
342
|
+
ugly1 ||= true
|
343
|
+
ugly2 ||= nil
|
344
|
+
|
345
|
+
nice1 + 1
|
346
|
+
nice2 + 1
|
347
|
+
ugly1 + 1
|
348
|
+
Ops.add(ugly2, 1)
|
349
|
+
```
|
350
|
+
|
351
|
+
Calls Preserving Niceness
|
352
|
+
-------------------------
|
353
|
+
|
354
|
+
A localized string literal is nice.
|
355
|
+
|
356
|
+
**Original**
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
v = _("Hello")
|
360
|
+
Ops.add(v, "World")
|
361
|
+
```
|
362
|
+
|
363
|
+
**Translated**
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
v = _("Hello")
|
367
|
+
v + "World"
|
368
|
+
```
|
369
|
+
|
370
|
+
### Calls Generating Niceness ###
|
371
|
+
|
372
|
+
`nil?` makes any value a nice value but unfortunately it seems of
|
373
|
+
little practical use. Even though there are two zombies that have
|
374
|
+
boolean arguments (`Builtins.find` and `Builtins.filter`), they are
|
375
|
+
just fine with `nil` since it is a falsey value.
|
376
|
+
|
377
|
+
Translation Below Top Level
|
378
|
+
---------------------------
|
379
|
+
|
380
|
+
Zombie Killer translates a zombie nested in other calls.
|
381
|
+
|
382
|
+
**Original**
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
v = 1
|
386
|
+
foo(bar(Ops.add(v, 1), baz))
|
387
|
+
```
|
388
|
+
|
389
|
+
**Translated**
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
v = 1
|
393
|
+
foo(bar(v + 1, baz))
|
394
|
+
```
|
395
|
+
|
396
|
+
Chained Translation
|
397
|
+
-------------------
|
398
|
+
|
399
|
+
Zombie Killer translates a left-associative chain of nice zombies.
|
400
|
+
|
401
|
+
**Original**
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
Ops.add(Ops.add(1, 2), 3)
|
405
|
+
```
|
406
|
+
|
407
|
+
**Translated**
|
408
|
+
|
409
|
+
```ruby
|
410
|
+
(1 + 2) + 3
|
411
|
+
```
|
412
|
+
|
413
|
+
Zombie Killer translates a right-associative chain of nice zombies.
|
414
|
+
|
415
|
+
**Original**
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
Ops.add(1, Ops.add(2, 3))
|
419
|
+
```
|
420
|
+
|
421
|
+
**Translated**
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
1 + (2 + 3)
|
425
|
+
```
|
426
|
+
|
427
|
+
### In case arguments are translated already
|
428
|
+
|
429
|
+
Zombie Killer translates `Ops.add` of plus and literal.
|
430
|
+
|
431
|
+
**Original**
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
Ops.add("Hello" + " ", "World")
|
435
|
+
```
|
436
|
+
|
437
|
+
**Translated**
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
("Hello" + " ") + "World"
|
441
|
+
```
|
442
|
+
|
443
|
+
Zombie Killer translates `Ops.add` of parenthesized plus and literal.
|
444
|
+
|
445
|
+
**Original**
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
Ops.add(("Hello" + " "), "World")
|
449
|
+
```
|
450
|
+
|
451
|
+
**Translated**
|
452
|
+
|
453
|
+
```ruby
|
454
|
+
("Hello" + " ") + "World"
|
455
|
+
```
|
456
|
+
|
457
|
+
Zombie Killer translates `Ops.add` of literal and plus.
|
458
|
+
|
459
|
+
**Original**
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
Ops.add("Hello", " " + "World")
|
463
|
+
```
|
464
|
+
|
465
|
+
**Translated**
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
"Hello" + (" " + "World")
|
469
|
+
```
|
470
|
+
|
471
|
+
If
|
472
|
+
--
|
473
|
+
|
474
|
+
With a **single-pass top-down data flow analysis**, that we have been using,
|
475
|
+
we can process the `if` statement but not beyond it,
|
476
|
+
because we cannot know which branch was taken.
|
477
|
+
|
478
|
+
We can proceed after the `if` statement but must **start with a clean slate**.
|
479
|
+
More precisely we should remove knowledge of all variables affected in either
|
480
|
+
branch of the `if` statement, but we will first simplify the job and wipe all
|
481
|
+
state for the processed method.
|
482
|
+
|
483
|
+
Zombie Killer translates the `then` body of an `if` statement.
|
484
|
+
|
485
|
+
**Original**
|
486
|
+
|
487
|
+
```ruby
|
488
|
+
if cond
|
489
|
+
Ops.add(1, 1)
|
490
|
+
end
|
491
|
+
```
|
492
|
+
|
493
|
+
**Translated**
|
494
|
+
|
495
|
+
```ruby
|
496
|
+
if cond
|
497
|
+
1 + 1
|
498
|
+
end
|
499
|
+
```
|
500
|
+
|
501
|
+
Zombie Killer translates the `then` body of an `unless` statement.
|
502
|
+
|
503
|
+
**Original**
|
504
|
+
|
505
|
+
```ruby
|
506
|
+
unless cond
|
507
|
+
Ops.add(1, 1)
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
**Translated**
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
unless cond
|
515
|
+
1 + 1
|
516
|
+
end
|
517
|
+
```
|
518
|
+
|
519
|
+
It translates both branches of an `if` statement, independently of each other.
|
520
|
+
|
521
|
+
**Original**
|
522
|
+
|
523
|
+
```ruby
|
524
|
+
v = 1
|
525
|
+
if cond
|
526
|
+
Ops.add(v, 1)
|
527
|
+
v = nil
|
528
|
+
else
|
529
|
+
Ops.add(1, v)
|
530
|
+
v = nil
|
531
|
+
end
|
532
|
+
```
|
533
|
+
|
534
|
+
**Translated**
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
v = 1
|
538
|
+
if cond
|
539
|
+
v + 1
|
540
|
+
v = nil
|
541
|
+
else
|
542
|
+
1 + v
|
543
|
+
v = nil
|
544
|
+
end
|
545
|
+
```
|
546
|
+
|
547
|
+
The condition also contributes to the data state.
|
548
|
+
|
549
|
+
**Original**
|
550
|
+
|
551
|
+
```ruby
|
552
|
+
if cond(v = 1)
|
553
|
+
Ops.add(v, 1)
|
554
|
+
end
|
555
|
+
```
|
556
|
+
|
557
|
+
**Translated**
|
558
|
+
|
559
|
+
```ruby
|
560
|
+
if cond(v = 1)
|
561
|
+
v + 1
|
562
|
+
end
|
563
|
+
```
|
564
|
+
|
565
|
+
### A variable is not nice after its niceness was invalidated by an `if`
|
566
|
+
|
567
|
+
Plain `if`
|
568
|
+
|
569
|
+
**Unchanged**
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
v = 1
|
573
|
+
if cond
|
574
|
+
v = nil
|
575
|
+
end
|
576
|
+
Ops.add(v, 1)
|
577
|
+
```
|
578
|
+
|
579
|
+
Trailing `if`.
|
580
|
+
|
581
|
+
**Unchanged**
|
582
|
+
|
583
|
+
```ruby
|
584
|
+
v = 1
|
585
|
+
v = nil if cond
|
586
|
+
Ops.add(v, 1)
|
587
|
+
```
|
588
|
+
|
589
|
+
Plain `unless`.
|
590
|
+
|
591
|
+
**Unchanged**
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
v = 1
|
595
|
+
unless cond
|
596
|
+
v = nil
|
597
|
+
end
|
598
|
+
Ops.add(v, 1)
|
599
|
+
```
|
600
|
+
|
601
|
+
Trailing `unless`.
|
602
|
+
|
603
|
+
**Unchanged**
|
604
|
+
|
605
|
+
```ruby
|
606
|
+
v = 1
|
607
|
+
v = nil unless cond
|
608
|
+
Ops.add(v, 1)
|
609
|
+
```
|
610
|
+
|
611
|
+
### Resuming with a clean slate after an `if`
|
612
|
+
|
613
|
+
It translates zombies whose arguments were found nice after an `if`.
|
614
|
+
|
615
|
+
**Original**
|
616
|
+
|
617
|
+
```ruby
|
618
|
+
if cond
|
619
|
+
v = nil
|
620
|
+
end
|
621
|
+
v = 1
|
622
|
+
Ops.add(v, 1)
|
623
|
+
```
|
624
|
+
|
625
|
+
**Translated**
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
if cond
|
629
|
+
v = nil
|
630
|
+
end
|
631
|
+
v = 1
|
632
|
+
v + 1
|
633
|
+
```
|
634
|
+
|
635
|
+
Case
|
636
|
+
----
|
637
|
+
|
638
|
+
With a **single-pass top-down data flow analysis**, that we have been using,
|
639
|
+
we can process the `case` statement but not beyond it,
|
640
|
+
because we cannot know which branch was taken.
|
641
|
+
|
642
|
+
We can proceed after the `case` statement but must **start with a clean slate**.
|
643
|
+
More precisely we should remove knowledge of all variables affected in either
|
644
|
+
branch of the `case` statement, but we will first simplify the job and wipe all
|
645
|
+
state for the processed method.
|
646
|
+
|
647
|
+
Zombie Killer translates the `when` body of a `case` statement.
|
648
|
+
|
649
|
+
**Original**
|
650
|
+
|
651
|
+
```ruby
|
652
|
+
case expr
|
653
|
+
when 1
|
654
|
+
Ops.add(1, 1)
|
655
|
+
end
|
656
|
+
```
|
657
|
+
|
658
|
+
**Translated**
|
659
|
+
|
660
|
+
```ruby
|
661
|
+
case expr
|
662
|
+
when 1
|
663
|
+
1 + 1
|
664
|
+
end
|
665
|
+
```
|
666
|
+
|
667
|
+
It translates all branches of a `case` statement, independently of each other.
|
668
|
+
|
669
|
+
**Original**
|
670
|
+
|
671
|
+
```ruby
|
672
|
+
v = 1
|
673
|
+
case expr
|
674
|
+
when 1
|
675
|
+
Ops.add(v, 1)
|
676
|
+
v = nil
|
677
|
+
when 2
|
678
|
+
Ops.add(v, 2)
|
679
|
+
v = nil
|
680
|
+
else
|
681
|
+
Ops.add(1, v)
|
682
|
+
v = nil
|
683
|
+
end
|
684
|
+
```
|
685
|
+
|
686
|
+
**Translated**
|
687
|
+
|
688
|
+
```ruby
|
689
|
+
v = 1
|
690
|
+
case expr
|
691
|
+
when 1
|
692
|
+
v + 1
|
693
|
+
v = nil
|
694
|
+
when 2
|
695
|
+
v + 2
|
696
|
+
v = nil
|
697
|
+
else
|
698
|
+
1 + v
|
699
|
+
v = nil
|
700
|
+
end
|
701
|
+
```
|
702
|
+
|
703
|
+
The expression also contributes to the data state.
|
704
|
+
|
705
|
+
**Original**
|
706
|
+
|
707
|
+
```ruby
|
708
|
+
case v = 1
|
709
|
+
when 1
|
710
|
+
Ops.add(v, 1)
|
711
|
+
end
|
712
|
+
```
|
713
|
+
|
714
|
+
**Translated**
|
715
|
+
|
716
|
+
```ruby
|
717
|
+
case v = 1
|
718
|
+
when 1
|
719
|
+
v + 1
|
720
|
+
end
|
721
|
+
```
|
722
|
+
|
723
|
+
The test also contributes to the data state.
|
724
|
+
|
725
|
+
**Original**
|
726
|
+
|
727
|
+
```ruby
|
728
|
+
case expr
|
729
|
+
when v = 1
|
730
|
+
Ops.add(v, 1)
|
731
|
+
end
|
732
|
+
```
|
733
|
+
|
734
|
+
**Translated**
|
735
|
+
|
736
|
+
```ruby
|
737
|
+
case expr
|
738
|
+
when v = 1
|
739
|
+
v + 1
|
740
|
+
end
|
741
|
+
```
|
742
|
+
|
743
|
+
### A variable is not nice after its niceness was invalidated by a `case`
|
744
|
+
|
745
|
+
**Unchanged**
|
746
|
+
|
747
|
+
```ruby
|
748
|
+
v = 1
|
749
|
+
case expr
|
750
|
+
when 1
|
751
|
+
v = nil
|
752
|
+
end
|
753
|
+
Ops.add(v, 1)
|
754
|
+
```
|
755
|
+
|
756
|
+
### Resuming with a clean slate after a `case`
|
757
|
+
|
758
|
+
It translates zombies whose arguments were found nice after a `case`.
|
759
|
+
|
760
|
+
**Original**
|
761
|
+
|
762
|
+
```ruby
|
763
|
+
case expr
|
764
|
+
when 1
|
765
|
+
v = nil
|
766
|
+
end
|
767
|
+
v = 1
|
768
|
+
Ops.add(v, 1)
|
769
|
+
```
|
770
|
+
|
771
|
+
**Translated**
|
772
|
+
|
773
|
+
```ruby
|
774
|
+
case expr
|
775
|
+
when 1
|
776
|
+
v = nil
|
777
|
+
end
|
778
|
+
v = 1
|
779
|
+
v + 1
|
780
|
+
```
|
781
|
+
|
782
|
+
Loops
|
783
|
+
-----
|
784
|
+
|
785
|
+
### While and Until
|
786
|
+
|
787
|
+
`while` and its negated twin `until` are loops
|
788
|
+
which means assignments later in its body can affect values
|
789
|
+
earlier in its body and in the condition. Therefore we cannot process either
|
790
|
+
one and we must clear the state afterwards.
|
791
|
+
|
792
|
+
Zombie Killer does not translate anything in the outer scope
|
793
|
+
that contains a `while`.
|
794
|
+
|
795
|
+
**Unchanged**
|
796
|
+
|
797
|
+
```ruby
|
798
|
+
v = 1
|
799
|
+
while Ops.add(v, 1)
|
800
|
+
Ops.add(1, 1)
|
801
|
+
end
|
802
|
+
Ops.add(v, 1)
|
803
|
+
```
|
804
|
+
|
805
|
+
Zombie Killer does not translate anything in the outer scope
|
806
|
+
that contains an `until`.
|
807
|
+
|
808
|
+
**Unchanged**
|
809
|
+
|
810
|
+
```ruby
|
811
|
+
v = 1
|
812
|
+
until Ops.add(v, 1)
|
813
|
+
Ops.add(1, 1)
|
814
|
+
end
|
815
|
+
Ops.add(v, 1)
|
816
|
+
```
|
817
|
+
|
818
|
+
Zombie Killer can continue processing after a `while`. Pun!
|
819
|
+
|
820
|
+
**Original**
|
821
|
+
|
822
|
+
```ruby
|
823
|
+
while cond
|
824
|
+
foo
|
825
|
+
end
|
826
|
+
v = 1
|
827
|
+
Ops.add(v, 1)
|
828
|
+
```
|
829
|
+
|
830
|
+
**Translated**
|
831
|
+
|
832
|
+
```ruby
|
833
|
+
while cond
|
834
|
+
foo
|
835
|
+
end
|
836
|
+
v = 1
|
837
|
+
v + 1
|
838
|
+
```
|
839
|
+
|
840
|
+
Zombie Killer can continue processing after an `until`. No pun.
|
841
|
+
|
842
|
+
**Original**
|
843
|
+
|
844
|
+
```ruby
|
845
|
+
until cond
|
846
|
+
foo
|
847
|
+
end
|
848
|
+
v = 1
|
849
|
+
Ops.add(v, 1)
|
850
|
+
```
|
851
|
+
|
852
|
+
**Translated**
|
853
|
+
|
854
|
+
```ruby
|
855
|
+
until cond
|
856
|
+
foo
|
857
|
+
end
|
858
|
+
v = 1
|
859
|
+
v + 1
|
860
|
+
```
|
861
|
+
|
862
|
+
Zombie Killer can parse both the syntactic and semantic post-condition.
|
863
|
+
|
864
|
+
**Unchanged**
|
865
|
+
|
866
|
+
```ruby
|
867
|
+
body_runs_after_condition while cond
|
868
|
+
body_runs_after_condition until cond
|
869
|
+
|
870
|
+
begin
|
871
|
+
body_runs_before_condition
|
872
|
+
end while cond
|
873
|
+
|
874
|
+
begin
|
875
|
+
body_runs_before_condition
|
876
|
+
end until cond
|
877
|
+
```
|
878
|
+
|
879
|
+
### For
|
880
|
+
|
881
|
+
`for` loops are just syntax sugar for an `each` call with a block. Thus, we need
|
882
|
+
to treat them as blocks.
|
883
|
+
|
884
|
+
Zombie Killer does not translate inside a `for` and resumes with a clean slate.
|
885
|
+
|
886
|
+
**Original**
|
887
|
+
|
888
|
+
```ruby
|
889
|
+
v = 1
|
890
|
+
v = Ops.add(v, 1)
|
891
|
+
|
892
|
+
for i in [1, 2, 3]
|
893
|
+
v = Ops.add(v, 1)
|
894
|
+
v = uglify
|
895
|
+
end
|
896
|
+
|
897
|
+
v = Ops.add(v, 1)
|
898
|
+
w = 1
|
899
|
+
w = Ops.add(w, 1)
|
900
|
+
```
|
901
|
+
|
902
|
+
**Translated**
|
903
|
+
|
904
|
+
```ruby
|
905
|
+
v = 1
|
906
|
+
v = v + 1
|
907
|
+
|
908
|
+
for i in [1, 2, 3]
|
909
|
+
v = Ops.add(v, 1)
|
910
|
+
v = uglify
|
911
|
+
end
|
912
|
+
|
913
|
+
v = Ops.add(v, 1)
|
914
|
+
w = 1
|
915
|
+
w = w + 1
|
916
|
+
```
|
917
|
+
|
918
|
+
Exceptions
|
919
|
+
----------
|
920
|
+
|
921
|
+
Raising an exception is not a problem at the `raise` site. There it means
|
922
|
+
that all remaining code in a `def` is skipped. It is a problem at the `rescue`
|
923
|
+
or `ensure` site where it means that *some* of the preceding code was not
|
924
|
+
executed.
|
925
|
+
|
926
|
+
Zombie Killer translates the parts, joining else, rescue separately.
|
927
|
+
|
928
|
+
**Original**
|
929
|
+
|
930
|
+
```ruby
|
931
|
+
def foo
|
932
|
+
v = 1
|
933
|
+
Ops.add(v, 1)
|
934
|
+
rescue
|
935
|
+
w = 1
|
936
|
+
Ops.add(w, 1)
|
937
|
+
v = nil
|
938
|
+
rescue
|
939
|
+
Ops.add(w, 1)
|
940
|
+
else
|
941
|
+
Ops.add(v, 1)
|
942
|
+
end
|
943
|
+
```
|
944
|
+
|
945
|
+
**Translated**
|
946
|
+
|
947
|
+
```ruby
|
948
|
+
def foo
|
949
|
+
v = 1
|
950
|
+
v + 1
|
951
|
+
rescue
|
952
|
+
w = 1
|
953
|
+
w + 1
|
954
|
+
v = nil
|
955
|
+
rescue
|
956
|
+
Ops.add(w, 1)
|
957
|
+
else
|
958
|
+
v + 1
|
959
|
+
end
|
960
|
+
```
|
961
|
+
|
962
|
+
### Skipping Code
|
963
|
+
|
964
|
+
Zombie Killer does not translate code that depends on niceness skipped
|
965
|
+
via an exception.
|
966
|
+
|
967
|
+
**Unchanged**
|
968
|
+
|
969
|
+
```ruby
|
970
|
+
def a_problem
|
971
|
+
v = nil
|
972
|
+
w = 1 / 0
|
973
|
+
v = 1
|
974
|
+
rescue
|
975
|
+
puts "Oops", Ops.add(v, 1)
|
976
|
+
end
|
977
|
+
```
|
978
|
+
|
979
|
+
### Exception Syntax
|
980
|
+
|
981
|
+
Zombie Killer can parse the syntactic variants of exception handling.
|
982
|
+
|
983
|
+
**Unchanged**
|
984
|
+
|
985
|
+
```ruby
|
986
|
+
begin
|
987
|
+
foo
|
988
|
+
raise "LOL"
|
989
|
+
foo
|
990
|
+
rescue Error
|
991
|
+
foo
|
992
|
+
rescue Bug, Blunder => b
|
993
|
+
foo
|
994
|
+
rescue => e
|
995
|
+
foo
|
996
|
+
rescue
|
997
|
+
foo
|
998
|
+
ensure
|
999
|
+
foo
|
1000
|
+
end
|
1001
|
+
yast rescue nil
|
1002
|
+
```
|
1003
|
+
|
1004
|
+
### Retry
|
1005
|
+
|
1006
|
+
The `retry` statement makes the begin-body effectively a loop which limits
|
1007
|
+
our translation possibilities.
|
1008
|
+
|
1009
|
+
Zombie Killer does not translate a begin-body when a rescue contains a retry.
|
1010
|
+
|
1011
|
+
**Unchanged**
|
1012
|
+
|
1013
|
+
```ruby
|
1014
|
+
def foo
|
1015
|
+
v = 1
|
1016
|
+
begin
|
1017
|
+
Ops.add(v, 1)
|
1018
|
+
maybe_raise
|
1019
|
+
rescue
|
1020
|
+
v = nil
|
1021
|
+
retry
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
```
|
1025
|
+
|
1026
|
+
Blocks
|
1027
|
+
------
|
1028
|
+
|
1029
|
+
Inside a block the data flow is more complex than we handle now.
|
1030
|
+
After it, we start anew.
|
1031
|
+
|
1032
|
+
Zombie Killer does not translate inside a block and resumes with a clean slate.
|
1033
|
+
|
1034
|
+
**Original**
|
1035
|
+
|
1036
|
+
```ruby
|
1037
|
+
v = 1
|
1038
|
+
v = Ops.add(v, 1)
|
1039
|
+
|
1040
|
+
2.times do
|
1041
|
+
v = Ops.add(v, 1)
|
1042
|
+
v = uglify
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
v = Ops.add(v, 1)
|
1046
|
+
w = 1
|
1047
|
+
w = Ops.add(w, 1)
|
1048
|
+
```
|
1049
|
+
|
1050
|
+
**Translated**
|
1051
|
+
|
1052
|
+
```ruby
|
1053
|
+
v = 1
|
1054
|
+
v = v + 1
|
1055
|
+
|
1056
|
+
2.times do
|
1057
|
+
v = Ops.add(v, 1)
|
1058
|
+
v = uglify
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
v = Ops.add(v, 1)
|
1062
|
+
w = 1
|
1063
|
+
w = w + 1
|
1064
|
+
```
|
1065
|
+
|
1066
|
+
Formatting
|
1067
|
+
----------
|
1068
|
+
|
1069
|
+
Zombie Killer does not translate `Ops.add` if any argument has a comment.
|
1070
|
+
|
1071
|
+
**Unchanged**
|
1072
|
+
|
1073
|
+
```ruby
|
1074
|
+
Ops.add(
|
1075
|
+
"Hello",
|
1076
|
+
# foo
|
1077
|
+
"World"
|
1078
|
+
)
|
1079
|
+
```
|
1080
|
+
|
1081
|
+
Templates
|
1082
|
+
---------
|
1083
|
+
|
1084
|
+
It translates.
|
1085
|
+
|
1086
|
+
**Original**
|
1087
|
+
|
1088
|
+
```ruby
|
1089
|
+
```
|
1090
|
+
|
1091
|
+
**Translated**
|
1092
|
+
|
1093
|
+
```ruby
|
1094
|
+
```
|
1095
|
+
|
1096
|
+
It does not translate.
|
1097
|
+
|
1098
|
+
**Unchanged**
|
1099
|
+
|
1100
|
+
```ruby
|
1101
|
+
```
|