furnace 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|