furnace 0.1.1 → 0.1.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.
- data/.gitignore +1 -0
- data/lib/furnace/cfg/graph.rb +46 -13
- data/lib/furnace/cfg/node.rb +17 -7
- data/lib/furnace/graphviz.rb +10 -3
- data/lib/furnace/transform.rb +0 -1
- data/lib/furnace/version.rb +1 -1
- metadata +3 -12
- data/lib/furnace/anf/edge.rb +0 -6
- data/lib/furnace/anf/graph.rb +0 -60
- data/lib/furnace/anf/if_node.rb +0 -17
- data/lib/furnace/anf/in_node.rb +0 -17
- data/lib/furnace/anf/let_node.rb +0 -37
- data/lib/furnace/anf/node.rb +0 -31
- data/lib/furnace/anf/return_node.rb +0 -17
- data/lib/furnace/transform/generic/anf_build.rb +0 -153
data/.gitignore
CHANGED
data/lib/furnace/cfg/graph.rb
CHANGED
@@ -5,10 +5,16 @@ module Furnace::CFG
|
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
@nodes = Set.new
|
8
|
+
|
9
|
+
@source_map = nil
|
10
|
+
@label_map = {}
|
8
11
|
end
|
9
12
|
|
10
13
|
def find_node(label)
|
11
|
-
if node = @
|
14
|
+
if node = @label_map[label]
|
15
|
+
node
|
16
|
+
elsif node = @nodes.find { |n| n.label == label }
|
17
|
+
@label_map[label] = node
|
12
18
|
node
|
13
19
|
else
|
14
20
|
raise "Cannot find CFG node #{label}"
|
@@ -16,19 +22,31 @@ module Furnace::CFG
|
|
16
22
|
end
|
17
23
|
|
18
24
|
def eliminate_unreachable!
|
19
|
-
|
20
|
-
|
21
|
-
node = worklist.first
|
22
|
-
worklist.delete node
|
25
|
+
queue = [entry]
|
26
|
+
reachable = Set[]
|
23
27
|
|
24
|
-
|
28
|
+
while queue.any?
|
29
|
+
node = queue.shift
|
30
|
+
reachable.add node
|
25
31
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
node.targets.each do |target|
|
33
|
+
unless reachable.include? target
|
34
|
+
queue.push target
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if node.exception
|
39
|
+
unless reachable.include? node.exception
|
40
|
+
queue.push node.exception
|
41
|
+
end
|
30
42
|
end
|
31
43
|
end
|
44
|
+
|
45
|
+
@nodes.each do |node|
|
46
|
+
@nodes.delete node unless reachable.include? node
|
47
|
+
end
|
48
|
+
|
49
|
+
flush
|
32
50
|
end
|
33
51
|
|
34
52
|
def merge_redundant!
|
@@ -41,7 +59,9 @@ module Furnace::CFG
|
|
41
59
|
next if target == @exit
|
42
60
|
|
43
61
|
if node.targets.count == 1 &&
|
44
|
-
target.sources.count == 1
|
62
|
+
target.sources.count == 1 &&
|
63
|
+
node.exception == target.exception
|
64
|
+
|
45
65
|
node.insns.delete node.cti
|
46
66
|
@nodes.delete node
|
47
67
|
@nodes.delete target
|
@@ -101,6 +121,9 @@ module Furnace::CFG
|
|
101
121
|
dom[source]
|
102
122
|
end.reduce(:&)
|
103
123
|
|
124
|
+
# An exception handler header node has no regular sources.
|
125
|
+
pred = [] if pred.nil?
|
126
|
+
|
104
127
|
current = Set[node].merge(pred)
|
105
128
|
if current != dom[node]
|
106
129
|
dom[node] = current
|
@@ -150,10 +173,11 @@ module Furnace::CFG
|
|
150
173
|
nodes.replace all_nodes
|
151
174
|
end
|
152
175
|
|
176
|
+
loops.default = nil
|
153
177
|
loops
|
154
178
|
end
|
155
179
|
|
156
|
-
def
|
180
|
+
def sources_for(node)
|
157
181
|
unless @source_map
|
158
182
|
@source_map = Hash.new { |h, k| h[k] = [] }
|
159
183
|
|
@@ -162,13 +186,18 @@ module Furnace::CFG
|
|
162
186
|
@source_map[target] << node
|
163
187
|
end
|
164
188
|
end
|
189
|
+
|
190
|
+
@source_map.each do |node, sources|
|
191
|
+
sources.freeze
|
192
|
+
end
|
165
193
|
end
|
166
194
|
|
167
|
-
@source_map
|
195
|
+
@source_map[node]
|
168
196
|
end
|
169
197
|
|
170
198
|
def flush
|
171
199
|
@source_map = nil
|
200
|
+
@label_map.clear
|
172
201
|
end
|
173
202
|
|
174
203
|
def to_graphviz
|
@@ -192,6 +221,10 @@ module Furnace::CFG
|
|
192
221
|
node.target_labels.each_with_index do |label, idx|
|
193
222
|
graph.edge node.label, label, "#{idx}"
|
194
223
|
end
|
224
|
+
|
225
|
+
if node.exception_label
|
226
|
+
graph.edge node.label, node.exception_label, "Exc", color: 'orange'
|
227
|
+
end
|
195
228
|
end
|
196
229
|
end
|
197
230
|
end
|
data/lib/furnace/cfg/node.rb
CHANGED
@@ -2,17 +2,23 @@ module Furnace::CFG
|
|
2
2
|
class Node
|
3
3
|
attr_reader :cfg, :label
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
alias :cti :control_transfer_instruction
|
5
|
+
attr_accessor :target_labels, :exception_label
|
6
|
+
attr_accessor :instructions, :control_transfer_instruction
|
8
7
|
|
9
|
-
|
8
|
+
alias :insns :instructions
|
9
|
+
alias :insns= :instructions=
|
10
|
+
alias :cti :control_transfer_instruction
|
11
|
+
alias :cti= :control_transfer_instruction=
|
12
|
+
|
13
|
+
def initialize(cfg, label=nil, insns=[], cti=nil,
|
14
|
+
target_labels=[], exception_label=nil)
|
10
15
|
@cfg, @label = cfg, label
|
11
16
|
|
12
17
|
@instructions = insns
|
13
18
|
@control_transfer_instruction = cti
|
14
19
|
|
15
|
-
@target_labels
|
20
|
+
@target_labels = target_labels
|
21
|
+
@exception_label = exception_label
|
16
22
|
end
|
17
23
|
|
18
24
|
def target_labels
|
@@ -22,7 +28,7 @@ module Furnace::CFG
|
|
22
28
|
def targets
|
23
29
|
@target_labels.map do |label|
|
24
30
|
@cfg.find_node label
|
25
|
-
end
|
31
|
+
end.freeze
|
26
32
|
end
|
27
33
|
|
28
34
|
def source_labels
|
@@ -30,7 +36,11 @@ module Furnace::CFG
|
|
30
36
|
end
|
31
37
|
|
32
38
|
def sources
|
33
|
-
@cfg.
|
39
|
+
@cfg.sources_for(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def exception
|
43
|
+
@cfg.find_node @exception_label if @exception_label
|
34
44
|
end
|
35
45
|
|
36
46
|
def exits?
|
data/lib/furnace/graphviz.rb
CHANGED
@@ -29,14 +29,21 @@ class Furnace::Graphviz
|
|
29
29
|
label: label
|
30
30
|
})
|
31
31
|
|
32
|
-
@code << %Q{#{name.inspect}
|
32
|
+
@code << %Q{#{name.inspect} #{graphviz_options(options)};\n}
|
33
33
|
end
|
34
34
|
|
35
|
-
def edge(from, to, label="")
|
36
|
-
|
35
|
+
def edge(from, to, label="", options={})
|
36
|
+
options = options.merge({
|
37
|
+
label: label.inspect
|
38
|
+
})
|
39
|
+
@code << %Q{#{from.inspect} -> #{to.inspect} #{graphviz_options(options)};\n}
|
37
40
|
end
|
38
41
|
|
39
42
|
def to_s
|
40
43
|
@code
|
41
44
|
end
|
45
|
+
|
46
|
+
def graphviz_options(options)
|
47
|
+
"[#{options.map { |k,v| "#{k}=#{v}" }.join(",")}]"
|
48
|
+
end
|
42
49
|
end
|
data/lib/furnace/transform.rb
CHANGED
@@ -9,6 +9,5 @@ require "furnace/transform/rubinius/ast_normalize"
|
|
9
9
|
require "furnace/transform/generic/label_normalize"
|
10
10
|
require "furnace/transform/generic/cfg_build"
|
11
11
|
require "furnace/transform/generic/cfg_normalize"
|
12
|
-
require "furnace/transform/generic/anf_build"
|
13
12
|
|
14
13
|
require "furnace/transform/optimizing/fold_constants"
|
data/lib/furnace/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: furnace
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-05-06 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Furnace is a static code analysis framework for dynamic languages, aimed
|
15
15
|
at efficient type and behavior inference.
|
@@ -28,13 +28,6 @@ files:
|
|
28
28
|
- bin/furnace
|
29
29
|
- furnace.gemspec
|
30
30
|
- lib/furnace.rb
|
31
|
-
- lib/furnace/anf/edge.rb
|
32
|
-
- lib/furnace/anf/graph.rb
|
33
|
-
- lib/furnace/anf/if_node.rb
|
34
|
-
- lib/furnace/anf/in_node.rb
|
35
|
-
- lib/furnace/anf/let_node.rb
|
36
|
-
- lib/furnace/anf/node.rb
|
37
|
-
- lib/furnace/anf/return_node.rb
|
38
31
|
- lib/furnace/ast.rb
|
39
32
|
- lib/furnace/ast/matcher.rb
|
40
33
|
- lib/furnace/ast/matcher/dsl.rb
|
@@ -55,7 +48,6 @@ files:
|
|
55
48
|
- lib/furnace/code/token.rb
|
56
49
|
- lib/furnace/graphviz.rb
|
57
50
|
- lib/furnace/transform.rb
|
58
|
-
- lib/furnace/transform/generic/anf_build.rb
|
59
51
|
- lib/furnace/transform/generic/cfg_build.rb
|
60
52
|
- lib/furnace/transform/generic/cfg_normalize.rb
|
61
53
|
- lib/furnace/transform/generic/label_normalize.rb
|
@@ -84,9 +76,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
76
|
version: '0'
|
85
77
|
requirements: []
|
86
78
|
rubyforge_project:
|
87
|
-
rubygems_version: 1.8.
|
79
|
+
rubygems_version: 1.8.23
|
88
80
|
signing_key:
|
89
81
|
specification_version: 3
|
90
82
|
summary: A static code analysis framework
|
91
83
|
test_files: []
|
92
|
-
has_rdoc:
|
data/lib/furnace/anf/edge.rb
DELETED
data/lib/furnace/anf/graph.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module ANF
|
3
|
-
class Graph
|
4
|
-
attr_reader :nodes, :edges
|
5
|
-
attr_accessor :root
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@root = nil
|
9
|
-
@nodes = Set.new
|
10
|
-
@edges = Set.new
|
11
|
-
end
|
12
|
-
|
13
|
-
def find(label)
|
14
|
-
@nodes.find { |node| node.label == label }
|
15
|
-
end
|
16
|
-
|
17
|
-
def eliminate_dead_code
|
18
|
-
live_set = search
|
19
|
-
@nodes &= live_set
|
20
|
-
end
|
21
|
-
|
22
|
-
def search
|
23
|
-
seen_set = Set.new
|
24
|
-
work_set = Set.new
|
25
|
-
|
26
|
-
work_set.add @root
|
27
|
-
|
28
|
-
while work_set.any?
|
29
|
-
node = work_set.first
|
30
|
-
work_set.delete node
|
31
|
-
seen_set.add node
|
32
|
-
|
33
|
-
yield node if block_given?
|
34
|
-
|
35
|
-
node.leaving_edges.map(&:target).each do |target|
|
36
|
-
work_set.add target unless seen_set.include? target
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
seen_set
|
41
|
-
end
|
42
|
-
|
43
|
-
def to_graphviz
|
44
|
-
Graphviz.new do |graph|
|
45
|
-
@nodes.each do |node|
|
46
|
-
graph.node node.object_id, node.to_human_readable
|
47
|
-
|
48
|
-
case node
|
49
|
-
when ANF::IfNode
|
50
|
-
graph.edge node.object_id, node.leaving_edge(true).target.object_id, "true"
|
51
|
-
graph.edge node.object_id, node.leaving_edge(false).target.object_id, "false"
|
52
|
-
when ANF::LetNode, ANF::InNode
|
53
|
-
graph.edge node.object_id, node.leaving_edge.target.object_id
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
data/lib/furnace/anf/if_node.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module ANF
|
3
|
-
class IfNode < Node
|
4
|
-
attr_reader :condition
|
5
|
-
|
6
|
-
def initialize(graph, condition)
|
7
|
-
super(graph)
|
8
|
-
|
9
|
-
@condition = condition
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_human_readable
|
13
|
-
"if\n#{humanize @condition}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
data/lib/furnace/anf/in_node.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module ANF
|
3
|
-
class InNode < Node
|
4
|
-
attr_reader :expressions
|
5
|
-
|
6
|
-
def initialize(graph, expressions)
|
7
|
-
super(graph)
|
8
|
-
|
9
|
-
@expressions = expressions
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_human_readable
|
13
|
-
"in\n#{@expressions.map { |e| "#{e.to_sexp(1)}" }.join "\n"}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
data/lib/furnace/anf/let_node.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module ANF
|
3
|
-
class LetNode < Node
|
4
|
-
attr_reader :arguments
|
5
|
-
|
6
|
-
def initialize(graph, arguments)
|
7
|
-
super(graph)
|
8
|
-
|
9
|
-
@arguments = arguments
|
10
|
-
end
|
11
|
-
|
12
|
-
def try_eliminate
|
13
|
-
if identity?
|
14
|
-
entering_edges.each do |edge|
|
15
|
-
edge.target = leaving_edge.target
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def identity?
|
21
|
-
@arguments.reduce(true) { |r, (k, v)| r && (v === k) }
|
22
|
-
end
|
23
|
-
|
24
|
-
def try_propagate
|
25
|
-
end
|
26
|
-
|
27
|
-
def static?(node)
|
28
|
-
[ NilClass, TrueClass, FalseClass, Fixnum, Symbol,
|
29
|
-
AST::LocalVariable, AST::InstanceVariable ].include? node.class
|
30
|
-
end
|
31
|
-
|
32
|
-
def to_human_readable
|
33
|
-
"let\n#{@arguments.map { |k, v| " #{k} = #{humanize v}" }.join "\n"}"
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
data/lib/furnace/anf/node.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module ANF
|
3
|
-
class Node
|
4
|
-
attr_reader :graph
|
5
|
-
|
6
|
-
def initialize(graph)
|
7
|
-
@graph = graph
|
8
|
-
end
|
9
|
-
|
10
|
-
def leaving_edges
|
11
|
-
@graph.edges.select { |edge| edge.source == self }
|
12
|
-
end
|
13
|
-
|
14
|
-
def leaving_edge(param=nil)
|
15
|
-
@graph.edges.find { |edge| edge.source == self && edge.param == param }
|
16
|
-
end
|
17
|
-
|
18
|
-
def entering_edges
|
19
|
-
@graph.edges.select { |edge| edge.target == self }
|
20
|
-
end
|
21
|
-
|
22
|
-
def humanize(node)
|
23
|
-
if node.is_a? AST::Node
|
24
|
-
node.to_sexp
|
25
|
-
else
|
26
|
-
node.inspect
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,153 +0,0 @@
|
|
1
|
-
module Furnace
|
2
|
-
module Transform
|
3
|
-
module Generic
|
4
|
-
class ANFBuild
|
5
|
-
include AST::Visitor
|
6
|
-
|
7
|
-
def transform(cfg, method)
|
8
|
-
@method_locals = method.local_names
|
9
|
-
|
10
|
-
@last_label = -1
|
11
|
-
@anf_nodes = Hash.new { |k,v| v }
|
12
|
-
@anf_edges = []
|
13
|
-
|
14
|
-
@anf = ANF::Graph.new
|
15
|
-
|
16
|
-
cfg.nodes.each do |node|
|
17
|
-
@locals = {}
|
18
|
-
@node = node
|
19
|
-
|
20
|
-
@last_anf_node = nil
|
21
|
-
|
22
|
-
# At this point we can have no more than two edges.
|
23
|
-
@default_edge = node.default_leaving_edge
|
24
|
-
@other_edge = node.leaving_edge(node.operations.last.metadata[:label])
|
25
|
-
|
26
|
-
# Transform the AST for each node to ANF, removing redundant root nodes
|
27
|
-
# in the process.
|
28
|
-
node.operations.delete_if do |operation|
|
29
|
-
visit operation
|
30
|
-
|
31
|
-
operation.type == :remove
|
32
|
-
end
|
33
|
-
|
34
|
-
# If there were no nodes created, fallback to default CFG edge.
|
35
|
-
@last_anf_node ||= @default_edge.target_label
|
36
|
-
|
37
|
-
# If some operations were done just for side effect, add an InNode.
|
38
|
-
if node.operations.any?
|
39
|
-
anf_in = ANF::InNode.new(@anf, node.operations)
|
40
|
-
@anf.nodes.add anf_in
|
41
|
-
|
42
|
-
@anf_edges << [ anf_in, @last_anf_node ]
|
43
|
-
@last_anf_node = anf_in
|
44
|
-
end
|
45
|
-
|
46
|
-
# If any locals were rebound, add a LetNode.
|
47
|
-
if @locals.any? || node.operations.any?
|
48
|
-
anf_let = ANF::LetNode.new(@anf, passed_locals)
|
49
|
-
@anf.nodes.add anf_let
|
50
|
-
|
51
|
-
@anf_edges << [ anf_let, @last_anf_node ]
|
52
|
-
@last_anf_node = anf_let
|
53
|
-
end
|
54
|
-
|
55
|
-
@anf_nodes[node.label] = @last_anf_node
|
56
|
-
end
|
57
|
-
|
58
|
-
# The root is a CFG node with label (ip) 0.
|
59
|
-
@anf.root = @anf_nodes[0]
|
60
|
-
|
61
|
-
@anf_edges.each do |(source_label, target_label, param)|
|
62
|
-
@anf.edges.add ANF::Edge.new(@anf_nodes[source_label],
|
63
|
-
@anf_nodes[target_label],
|
64
|
-
param)
|
65
|
-
end
|
66
|
-
|
67
|
-
[ @anf, method ]
|
68
|
-
end
|
69
|
-
|
70
|
-
def passed_locals
|
71
|
-
map = @method_locals.map do |name|
|
72
|
-
# Is the name rebound?
|
73
|
-
if @locals.include?(name)
|
74
|
-
[ name, @locals[name] ]
|
75
|
-
# Is it the middle of function?
|
76
|
-
elsif @node.entering_edges.any?
|
77
|
-
[ name, AST::LocalVariable.new(name) ]
|
78
|
-
# Locals default to nil.
|
79
|
-
else
|
80
|
-
[ name, nil ]
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
Hash[*map.flatten]
|
85
|
-
end
|
86
|
-
|
87
|
-
# (set-lvar :var value)
|
88
|
-
def on_set_lvar(ast_node)
|
89
|
-
@locals[ast_node.children.first] = ast_node.children.last
|
90
|
-
|
91
|
-
ast_node.update(:remove)
|
92
|
-
end
|
93
|
-
|
94
|
-
# (jump-if compare_to condition)
|
95
|
-
def on_jump_if(ast_node)
|
96
|
-
if ast_node.children.first == true
|
97
|
-
true_edge, false_edge = @other_edge, @default_edge
|
98
|
-
else
|
99
|
-
true_edge, false_edge = @default_edge, @other_edge
|
100
|
-
end
|
101
|
-
|
102
|
-
true_node = ANF::LetNode.new(@anf, passed_locals)
|
103
|
-
false_node = ANF::LetNode.new(@anf, passed_locals)
|
104
|
-
@last_anf_node = ANF::IfNode.new(@anf, ast_node.children.last)
|
105
|
-
|
106
|
-
@anf.nodes.merge [ true_node, false_node, @last_anf_node ]
|
107
|
-
|
108
|
-
@anf_edges << [ @last_anf_node, true_node, true ] <<
|
109
|
-
[ true_node, true_edge.target_label ]
|
110
|
-
@anf_edges << [ @last_anf_node, false_node, false ] <<
|
111
|
-
[ false_node, false_edge.target_label ]
|
112
|
-
|
113
|
-
ast_node.update(:remove)
|
114
|
-
end
|
115
|
-
|
116
|
-
# (return expression)
|
117
|
-
def on_return(ast_node)
|
118
|
-
@last_anf_node = ANF::ReturnNode.new(@anf, ast_node.children.last)
|
119
|
-
|
120
|
-
@anf.nodes.add @last_anf_node
|
121
|
-
|
122
|
-
ast_node.update(:remove)
|
123
|
-
end
|
124
|
-
|
125
|
-
# (get-lvar :x) -> %x
|
126
|
-
def on_get_lvar(node)
|
127
|
-
node.update(:expand, AST::LocalVariable.new(node.children.first))
|
128
|
-
end
|
129
|
-
|
130
|
-
# AST node labels do not make sense.
|
131
|
-
def on_any(ast_node)
|
132
|
-
ast_node.metadata.delete :label
|
133
|
-
end
|
134
|
-
|
135
|
-
def expand_node(node)
|
136
|
-
node.update(:expand)
|
137
|
-
end
|
138
|
-
|
139
|
-
# Immediates do not have to carry metadata anymore.
|
140
|
-
alias :on_true :expand_node
|
141
|
-
alias :on_false :expand_node
|
142
|
-
alias :on_nil :expand_node
|
143
|
-
alias :on_fixnum :expand_node
|
144
|
-
alias :on_literal :expand_node
|
145
|
-
|
146
|
-
# We have a near infinite supply of small, unoccupied labels.
|
147
|
-
def make_label
|
148
|
-
@last_label -= 1
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|