furnace 0.0.1
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 +5 -0
- data/.rvmrc +61 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/bin/furnace +22 -0
- data/furnace.gemspec +19 -0
- data/lib/furnace/anf/edge.rb +6 -0
- data/lib/furnace/anf/graph.rb +60 -0
- data/lib/furnace/anf/if_node.rb +17 -0
- data/lib/furnace/anf/in_node.rb +17 -0
- data/lib/furnace/anf/let_node.rb +37 -0
- data/lib/furnace/anf/node.rb +31 -0
- data/lib/furnace/anf/return_node.rb +17 -0
- data/lib/furnace/ast/node.rb +70 -0
- data/lib/furnace/ast/symbolic_node.rb +36 -0
- data/lib/furnace/ast/visitor.rb +42 -0
- data/lib/furnace/cfg/edge.rb +36 -0
- data/lib/furnace/cfg/graph.rb +59 -0
- data/lib/furnace/cfg/node.rb +39 -0
- data/lib/furnace/graphviz.rb +30 -0
- data/lib/furnace/transform/generic/anf_build.rb +153 -0
- data/lib/furnace/transform/generic/cfg_build.rb +89 -0
- data/lib/furnace/transform/generic/cfg_normalize.rb +41 -0
- data/lib/furnace/transform/generic/label_normalize.rb +60 -0
- data/lib/furnace/transform/optimizing/fold_constants.rb +19 -0
- data/lib/furnace/transform/pipeline.rb +17 -0
- data/lib/furnace/transform/rubinius/ast_build.rb +53 -0
- data/lib/furnace/transform/rubinius/ast_normalize.rb +170 -0
- data/lib/furnace/version.rb +3 -0
- data/lib/furnace.rb +37 -0
- metadata +95 -0
data/.rvmrc
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
|
7
|
+
environment_id="rbx-head@furnace"
|
8
|
+
|
9
|
+
#
|
10
|
+
# Uncomment following line if you want options to be set only for given project.
|
11
|
+
#
|
12
|
+
# PROJECT_JRUBY_OPTS=( --1.9 )
|
13
|
+
|
14
|
+
#
|
15
|
+
# First we attempt to load the desired environment directly from the environment
|
16
|
+
# file. This is very fast and efficient compared to running through the entire
|
17
|
+
# CLI and selector. If you want feedback on which environment was used then
|
18
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
19
|
+
#
|
20
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
|
21
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
22
|
+
then
|
23
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
24
|
+
|
25
|
+
if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
|
26
|
+
then
|
27
|
+
. "${rvm_path:-$HOME/.rvm}/hooks/after_use"
|
28
|
+
fi
|
29
|
+
else
|
30
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
31
|
+
if ! rvm --create "$environment_id"
|
32
|
+
then
|
33
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
34
|
+
return 1
|
35
|
+
fi
|
36
|
+
fi
|
37
|
+
|
38
|
+
#
|
39
|
+
# If you use an RVM gemset file to install a list of gems (*.gems), you can have
|
40
|
+
# it be automatically loaded. Uncomment the following and adjust the filename if
|
41
|
+
# necessary.
|
42
|
+
#
|
43
|
+
# filename=".gems"
|
44
|
+
# if [[ -s "$filename" ]]
|
45
|
+
# then
|
46
|
+
# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
|
47
|
+
# fi
|
48
|
+
|
49
|
+
# If you use bundler, this might be useful to you:
|
50
|
+
# if command -v bundle && [[ -s Gemfile ]]
|
51
|
+
# then
|
52
|
+
# bundle install
|
53
|
+
# fi
|
54
|
+
|
55
|
+
if [[ $- == *i* ]] # check for interactive shells
|
56
|
+
then
|
57
|
+
echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
|
58
|
+
else
|
59
|
+
echo "Using: $GEM_HOME" # don't use colors in interactive shells
|
60
|
+
fi
|
61
|
+
|
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/furnace
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'furnace'
|
4
|
+
|
5
|
+
load ARGV[0]
|
6
|
+
|
7
|
+
pipeline = Furnace::Transform::Pipeline.new(*[
|
8
|
+
Furnace::Transform::Rubinius::ASTBuild.new,
|
9
|
+
Furnace::Transform::Rubinius::ASTNormalize.new,
|
10
|
+
|
11
|
+
Furnace::Transform::Generic::LabelNormalize.new,
|
12
|
+
Furnace::Transform::Generic::CFGBuild.new,
|
13
|
+
Furnace::Transform::Generic::CFGNormalize.new,
|
14
|
+
|
15
|
+
Furnace::Transform::Generic::ANFBuild.new,
|
16
|
+
|
17
|
+
Furnace::Transform::Optimizing::FoldConstants.new,
|
18
|
+
])
|
19
|
+
|
20
|
+
cfg, = pipeline.run(method(:main).executable)
|
21
|
+
|
22
|
+
puts cfg.to_graphviz
|
data/furnace.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "furnace/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "furnace"
|
7
|
+
s.version = Furnace::VERSION
|
8
|
+
s.authors = ["Peter Zotov"]
|
9
|
+
s.email = ["whitequark@whitequark.org"]
|
10
|
+
s.homepage = "http://github.com/whitequark/furnace"
|
11
|
+
s.summary = %q{A static Ruby compiler}
|
12
|
+
s.description = %q{Furnace aims to compile Ruby code into small static } <<
|
13
|
+
%q{executables by restricting its metaprogramming features.}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,37 @@
|
|
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
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Furnace
|
2
|
+
module AST
|
3
|
+
class Node
|
4
|
+
attr_accessor :type, :parent, :children, :metadata
|
5
|
+
|
6
|
+
def initialize(type, children=[], metadata={})
|
7
|
+
@type, @children, @metadata = type.to_sym, children, metadata
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(type, children=nil, metadata={})
|
11
|
+
@type = type
|
12
|
+
@children = children || @children
|
13
|
+
|
14
|
+
# If something non-nil is passed, including default value, then merge.
|
15
|
+
# Else, clear metadata store.
|
16
|
+
if metadata
|
17
|
+
@metadata.merge!(metadata)
|
18
|
+
else
|
19
|
+
@metadata = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def index
|
26
|
+
parent.children.find_index(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def next
|
30
|
+
parent.children[index + 1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def prev
|
34
|
+
parent.children[index - 1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"(#{fancy_type} ...)"
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_sexp(indent=0)
|
42
|
+
str = "#{" " * indent}(#{fancy_type}"
|
43
|
+
|
44
|
+
children.each do |child|
|
45
|
+
if child.is_a? Node
|
46
|
+
str << "\n#{child.to_sexp(indent + 1)}"
|
47
|
+
else
|
48
|
+
str << " #{child.inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
str << ")"
|
53
|
+
|
54
|
+
str
|
55
|
+
end
|
56
|
+
alias :inspect :to_sexp
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def fancy_type
|
61
|
+
dasherized = @type.to_s.gsub('_', '-')
|
62
|
+
if @metadata[:label]
|
63
|
+
"#{@metadata[:label]}:#{dasherized}"
|
64
|
+
else
|
65
|
+
dasherized
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Furnace
|
2
|
+
module AST
|
3
|
+
class SymbolicNode
|
4
|
+
def initialize(name)
|
5
|
+
@name = name.to_sym
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_sym
|
9
|
+
@name
|
10
|
+
end
|
11
|
+
|
12
|
+
def ===(name)
|
13
|
+
@name == name.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
@name.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MethodName < SymbolicNode
|
22
|
+
def inspect
|
23
|
+
".#{@name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class LocalVariable < SymbolicNode
|
28
|
+
def inspect
|
29
|
+
"%#{@name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Constant < SymbolicNode
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Furnace
|
2
|
+
module AST
|
3
|
+
module Visitor
|
4
|
+
def visit(node)
|
5
|
+
node.children.map! do |child|
|
6
|
+
if child.is_a? Node
|
7
|
+
visit child
|
8
|
+
|
9
|
+
if child.type == :expand
|
10
|
+
child = child.children
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
child
|
15
|
+
end
|
16
|
+
|
17
|
+
node.children.flatten!
|
18
|
+
|
19
|
+
node.children.delete_if do |child|
|
20
|
+
if child.is_a? Node
|
21
|
+
child.parent = node
|
22
|
+
|
23
|
+
child.type == :remove
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Invoke a specific handler
|
28
|
+
on_handler = :"on_#{node.type}"
|
29
|
+
if respond_to? on_handler
|
30
|
+
send on_handler, node
|
31
|
+
end
|
32
|
+
|
33
|
+
# Invoke a generic handler
|
34
|
+
if respond_to? :on_any
|
35
|
+
send :on_any, node
|
36
|
+
end
|
37
|
+
|
38
|
+
node
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Furnace
|
2
|
+
module CFG
|
3
|
+
class Edge
|
4
|
+
attr_accessor :source_operation, :source_label, :target_label
|
5
|
+
|
6
|
+
def initialize(cfg, source_operation, source_label, target_label)
|
7
|
+
@cfg, @source_operation, @source_label, @target_label =
|
8
|
+
cfg, source_operation, source_label, target_label
|
9
|
+
end
|
10
|
+
|
11
|
+
def source
|
12
|
+
@cfg.find_node(@source_label)
|
13
|
+
end
|
14
|
+
|
15
|
+
def target
|
16
|
+
@cfg.find_node(@target_label) if @target_label
|
17
|
+
end
|
18
|
+
|
19
|
+
def source=(node)
|
20
|
+
@source_label = node.label
|
21
|
+
end
|
22
|
+
|
23
|
+
def target=(node)
|
24
|
+
if node
|
25
|
+
@target_label = node.label
|
26
|
+
else
|
27
|
+
@target_label = nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"<#{@source_label.inspect} -> #{@target_label.inspect}>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Furnace
|
2
|
+
module CFG
|
3
|
+
class Graph
|
4
|
+
attr_reader :nodes, :edges
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@nodes = Set.new
|
8
|
+
@edges = Set.new
|
9
|
+
|
10
|
+
@pending_label = nil
|
11
|
+
@pending_operations = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_node(label)
|
15
|
+
if node = @nodes.find { |n| n.label == label }
|
16
|
+
node
|
17
|
+
else
|
18
|
+
raise "Cannot find CFG node #{label}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def expand(label, operation)
|
23
|
+
@pending_label ||= label
|
24
|
+
@pending_operations << operation
|
25
|
+
end
|
26
|
+
|
27
|
+
def transfer(targets)
|
28
|
+
return unless @pending_label
|
29
|
+
|
30
|
+
@nodes << CFG::Node.new(self, @pending_label, @pending_operations)
|
31
|
+
|
32
|
+
targets.each do |operation, target|
|
33
|
+
@edges << CFG::Edge.new(self, operation, @pending_label, target)
|
34
|
+
end
|
35
|
+
|
36
|
+
@pending_label = nil
|
37
|
+
@pending_operations = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_graphviz
|
41
|
+
Graphviz.new do |graph|
|
42
|
+
@nodes.each do |node|
|
43
|
+
graph.node node.label, node.operations.map(&:inspect).join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
@edges.each do |edge|
|
47
|
+
if edge.source_operation.nil?
|
48
|
+
label = "~"
|
49
|
+
else
|
50
|
+
label = edge.source_operation
|
51
|
+
end
|
52
|
+
|
53
|
+
graph.edge edge.source_label, edge.target_label, label
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Furnace
|
2
|
+
module CFG
|
3
|
+
class Node
|
4
|
+
attr_reader :label, :operations
|
5
|
+
|
6
|
+
def initialize(cfg, label, operations)
|
7
|
+
@cfg, @label, @operations = cfg, label, operations
|
8
|
+
end
|
9
|
+
|
10
|
+
def entering_edges
|
11
|
+
@cfg.edges.select { |e| e.target == self }
|
12
|
+
end
|
13
|
+
|
14
|
+
def leaving_edges
|
15
|
+
@cfg.edges.select { |e| e.source == self }
|
16
|
+
end
|
17
|
+
|
18
|
+
def leaving_edge(source)
|
19
|
+
leaving_edges.find { |e| e.source_operation == source }
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_leaving_edge
|
23
|
+
leaving_edge(nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
self.label == other.label
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
if @label
|
32
|
+
"<#{@label}:#{@operations.map(&:inspect).join ", "}>"
|
33
|
+
else
|
34
|
+
"<!exit>"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Furnace
|
2
|
+
class Graphviz
|
3
|
+
def initialize
|
4
|
+
@code = "digraph {\n"
|
5
|
+
@code << "node [labeljust=l,nojustify=true,fontname=monospace];"
|
6
|
+
@code << "rankdir=TB;"
|
7
|
+
|
8
|
+
yield self
|
9
|
+
|
10
|
+
@code << "}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def node(name, content)
|
14
|
+
content.gsub!("&", "&")
|
15
|
+
content.gsub!(">", ">")
|
16
|
+
content.gsub!("<", "<")
|
17
|
+
content = content.lines.map { |l| %Q{<tr><td align="left">#{l}</td></tr>} }.join
|
18
|
+
|
19
|
+
@code << %Q{#{name.inspect} [shape=box,label=<<table border="0">#{content}</table>>];\n}
|
20
|
+
end
|
21
|
+
|
22
|
+
def edge(from, to, label="")
|
23
|
+
@code << %Q{#{from.inspect} -> #{to.inspect} [label=#{label.inspect}];\n}
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@code
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|