sfrp 1.1.0
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/.ctags +3 -0
- data/.editorconfig +9 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +629 -0
- data/.travis.yml +12 -0
- data/Gemfile +2 -0
- data/LICENSE +28 -0
- data/README.md +34 -0
- data/Rakefile +1 -0
- data/base-library/Base.sfrp +81 -0
- data/base-library/IO/AVR/ATMEGA8.c +9 -0
- data/base-library/IO/AVR/ATMEGA8.h +6 -0
- data/base-library/IO/AVR/ATMEGA8.sfrp +4 -0
- data/base-library/IO/STDIO.c +40 -0
- data/base-library/IO/STDIO.h +13 -0
- data/base-library/IO/STDIO.sfrp +10 -0
- data/bin/sfrp +7 -0
- data/lib/sfrp.rb +2 -0
- data/lib/sfrp/command.rb +73 -0
- data/lib/sfrp/compiler.rb +94 -0
- data/lib/sfrp/error.rb +4 -0
- data/lib/sfrp/file.rb +18 -0
- data/lib/sfrp/flat/dsl.rb +33 -0
- data/lib/sfrp/flat/elements.rb +90 -0
- data/lib/sfrp/flat/exception.rb +45 -0
- data/lib/sfrp/flat/expression.rb +125 -0
- data/lib/sfrp/flat/set.rb +61 -0
- data/lib/sfrp/input/exception.rb +16 -0
- data/lib/sfrp/input/parser.rb +417 -0
- data/lib/sfrp/input/set.rb +29 -0
- data/lib/sfrp/input/transformer.rb +219 -0
- data/lib/sfrp/low/dsl.rb +126 -0
- data/lib/sfrp/low/element.rb +126 -0
- data/lib/sfrp/low/set.rb +62 -0
- data/lib/sfrp/mono/dsl.rb +120 -0
- data/lib/sfrp/mono/environment.rb +26 -0
- data/lib/sfrp/mono/exception.rb +21 -0
- data/lib/sfrp/mono/expression.rb +124 -0
- data/lib/sfrp/mono/function.rb +86 -0
- data/lib/sfrp/mono/memory.rb +32 -0
- data/lib/sfrp/mono/node.rb +125 -0
- data/lib/sfrp/mono/pattern.rb +69 -0
- data/lib/sfrp/mono/set.rb +151 -0
- data/lib/sfrp/mono/type.rb +210 -0
- data/lib/sfrp/mono/vconst.rb +134 -0
- data/lib/sfrp/output/set.rb +33 -0
- data/lib/sfrp/poly/dsl.rb +171 -0
- data/lib/sfrp/poly/elements.rb +168 -0
- data/lib/sfrp/poly/exception.rb +42 -0
- data/lib/sfrp/poly/expression.rb +170 -0
- data/lib/sfrp/poly/monofier.rb +73 -0
- data/lib/sfrp/poly/set.rb +90 -0
- data/lib/sfrp/poly/typing.rb +197 -0
- data/lib/sfrp/raw/dsl.rb +41 -0
- data/lib/sfrp/raw/elements.rb +164 -0
- data/lib/sfrp/raw/exception.rb +40 -0
- data/lib/sfrp/raw/expression.rb +168 -0
- data/lib/sfrp/raw/namespace.rb +30 -0
- data/lib/sfrp/raw/set.rb +109 -0
- data/lib/sfrp/version.rb +3 -0
- data/sfrp.gemspec +40 -0
- data/spec/sfrp/Test.sfrp +4 -0
- data/spec/sfrp/compiler_spec.rb +17 -0
- data/spec/sfrp/flat/set_spec.rb +40 -0
- data/spec/sfrp/input/parse_test.sfrp +20 -0
- data/spec/sfrp/input/set_spec.rb +18 -0
- data/spec/sfrp/low/set_spec.rb +20 -0
- data/spec/sfrp/mono/expected.yml +295 -0
- data/spec/sfrp/mono/set_spec.rb +152 -0
- data/spec/sfrp/output/set_spec.rb +29 -0
- data/spec/sfrp/poly/set_spec.rb +290 -0
- data/spec/sfrp/raw/set_spec.rb +38 -0
- data/spec/spec_helper.rb +16 -0
- data/test/IntTest/Main.c +5 -0
- data/test/IntTest/Main.h +6 -0
- data/test/IntTest/Main.sfrp +10 -0
- data/test/IntTest/in.txt +3 -0
- data/test/IntTest/out.txt +4 -0
- data/test/MaybeTest/Main.sfrp +8 -0
- data/test/MaybeTest/SubDir/Lib.sfrp +9 -0
- data/test/MaybeTest/in.txt +6 -0
- data/test/MaybeTest/out.txt +6 -0
- data/test/Rakefile +15 -0
- metadata +290 -0
data/lib/sfrp/low/set.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'sfrp/low/element'
|
2
|
+
require 'sfrp/low/dsl'
|
3
|
+
|
4
|
+
module SFRP
|
5
|
+
module Low
|
6
|
+
class Set
|
7
|
+
attr_reader :meta, :typedefs, :structs, :functions, :macros, :includes
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@typedefs = []
|
11
|
+
@structs = []
|
12
|
+
@functions = []
|
13
|
+
@macros = []
|
14
|
+
@includes = []
|
15
|
+
block.call(self) if block
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_output
|
19
|
+
Output::Set.new do |dest_set|
|
20
|
+
dest_set.create_file('main', 'c', main_file_content)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(element)
|
25
|
+
case element
|
26
|
+
when Typedef
|
27
|
+
@typedefs << element
|
28
|
+
when Structure
|
29
|
+
@structs << element
|
30
|
+
when Function
|
31
|
+
@functions << element
|
32
|
+
when Macro
|
33
|
+
@macros << element
|
34
|
+
when Include
|
35
|
+
@includes << element
|
36
|
+
else
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def main_file_content
|
44
|
+
elements = []
|
45
|
+
@includes.each { |x| elements << x.to_s }
|
46
|
+
@macros.each { |x| elements << x.to_s }
|
47
|
+
@typedefs.each { |x| elements << x.to_s }
|
48
|
+
@structs.each { |x| elements << x.to_s }
|
49
|
+
@functions.each do |x|
|
50
|
+
elements << x.pretty_code_prototype
|
51
|
+
end
|
52
|
+
@functions.each { |x| elements << x.to_s }
|
53
|
+
elements.join("\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def header_file_content
|
57
|
+
elements = []
|
58
|
+
elements.join("\n")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module SFRP
|
2
|
+
module Mono
|
3
|
+
module DSL
|
4
|
+
extend SFRP::M = self
|
5
|
+
|
6
|
+
def type(type_str, vconst_strs = nil, static = false, native_str = nil)
|
7
|
+
Type.new(type_str, vconst_strs, static, native_str)
|
8
|
+
end
|
9
|
+
|
10
|
+
def vconst(type_str, vconst_str, arg_type_strs, native_str = nil)
|
11
|
+
VConst.new(vconst_str, type_str, arg_type_strs, native_str)
|
12
|
+
end
|
13
|
+
|
14
|
+
def node(type_str, node_str, eval_func_str, init_func_str = nil, &block)
|
15
|
+
px = NodeDepProxy.new
|
16
|
+
block.call(px) if block
|
17
|
+
Node.new(node_str, type_str, px.to_a, eval_func_str, init_func_str)
|
18
|
+
end
|
19
|
+
|
20
|
+
def func(type_str, func_str, &block)
|
21
|
+
fp = FuncProxy.new
|
22
|
+
block.call(fp) if block
|
23
|
+
ftype = fp.ftype(type_str)
|
24
|
+
Function.new(func_str, fp.param_strs, ftype, fp.exp, fp.ffi_str)
|
25
|
+
end
|
26
|
+
|
27
|
+
def match_e(type_str, left_exp, &block)
|
28
|
+
cp = CaseProxy.new
|
29
|
+
block.call(cp) if block
|
30
|
+
MatchExp.new(type_str, left_exp, cp.to_a)
|
31
|
+
end
|
32
|
+
|
33
|
+
def call_e(type_str, func_str, *arg_exps)
|
34
|
+
FuncCallExp.new(type_str, func_str, arg_exps)
|
35
|
+
end
|
36
|
+
|
37
|
+
def vc_call_e(type_str, vconst_str, *arg_exps)
|
38
|
+
VConstCallExp.new(type_str, vconst_str, arg_exps)
|
39
|
+
end
|
40
|
+
|
41
|
+
def v_e(type_str, var_str)
|
42
|
+
VarRefExp.new(type_str, var_str)
|
43
|
+
end
|
44
|
+
|
45
|
+
def pat(type_str, vconst_str, *arg_patterns)
|
46
|
+
Pattern.new(type_str, vconst_str, nil, arg_patterns)
|
47
|
+
end
|
48
|
+
|
49
|
+
def pref(type_str, vconst_str, ref_var_str, *arg_patterns)
|
50
|
+
Pattern.new(type_str, vconst_str, ref_var_str, arg_patterns)
|
51
|
+
end
|
52
|
+
|
53
|
+
def pany(type_str, ref_var_str = nil)
|
54
|
+
Pattern.new(type_str, nil, ref_var_str, [])
|
55
|
+
end
|
56
|
+
|
57
|
+
class NodeDepProxy
|
58
|
+
def initialize
|
59
|
+
@node_refs = []
|
60
|
+
end
|
61
|
+
|
62
|
+
def l(node_str)
|
63
|
+
@node_refs << Node::NodeRef.new(node_str, true)
|
64
|
+
end
|
65
|
+
|
66
|
+
def c(node_str)
|
67
|
+
@node_refs << Node::NodeRef.new(node_str, false)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_a
|
71
|
+
@node_refs
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class CaseProxy
|
76
|
+
def initialize
|
77
|
+
@cases = []
|
78
|
+
end
|
79
|
+
|
80
|
+
def case(pattern, &exp_block)
|
81
|
+
@cases << MatchExp::Case.new(pattern, exp_block.call)
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_a
|
85
|
+
@cases
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class FuncProxy
|
90
|
+
def initialize
|
91
|
+
@param_type_strs = []
|
92
|
+
@param_strs = []
|
93
|
+
end
|
94
|
+
|
95
|
+
def param(type_str, param_str)
|
96
|
+
@param_type_strs << type_str
|
97
|
+
@param_strs << param_str
|
98
|
+
end
|
99
|
+
|
100
|
+
def exp(&exp_block)
|
101
|
+
@exp = exp_block.call if exp_block
|
102
|
+
@exp
|
103
|
+
end
|
104
|
+
|
105
|
+
def ffi_str(str = nil)
|
106
|
+
@ffi_str = str if str
|
107
|
+
@ffi_str
|
108
|
+
end
|
109
|
+
|
110
|
+
def param_strs
|
111
|
+
@param_strs
|
112
|
+
end
|
113
|
+
|
114
|
+
def ftype(return_type_str)
|
115
|
+
Function::FType.new(@param_type_strs, return_type_str)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SFRP
|
2
|
+
module Mono
|
3
|
+
class Environment
|
4
|
+
def initialize
|
5
|
+
@serial_queue = ('_v00'..'_v99').to_a
|
6
|
+
@var_str_to_type_str = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def new_var(type_str)
|
10
|
+
var = @serial_queue.shift
|
11
|
+
@var_str_to_type_str[var] = type_str
|
12
|
+
var
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_var(var_str, type_str)
|
16
|
+
@var_str_to_type_str[var_str] = type_str
|
17
|
+
end
|
18
|
+
|
19
|
+
def each_declared_vars(&block)
|
20
|
+
@var_str_to_type_str.each do |var_str, type_str|
|
21
|
+
block.call(var_str, type_str)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sfrp/error'
|
2
|
+
|
3
|
+
module SFRP
|
4
|
+
module Mono
|
5
|
+
class InvalidTypeOfForeignFunctionError < CompileError
|
6
|
+
def initialize(ffi_str)
|
7
|
+
@ffi_str = ffi_str
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"foreign function '#{@ffi_str}' returns invalid type'"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class IncompleteMatchExpError < CompileError
|
16
|
+
def message
|
17
|
+
"incomplete match-exp"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module SFRP
|
2
|
+
module Mono
|
3
|
+
class Exp
|
4
|
+
attr_reader :type_str
|
5
|
+
|
6
|
+
def ==(other)
|
7
|
+
comp == other.comp
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MatchExp < Exp
|
12
|
+
Case = Struct.new(:pattern, :exp)
|
13
|
+
|
14
|
+
def initialize(type_str, left_exp, cases, id = nil)
|
15
|
+
@type_str = type_str
|
16
|
+
@left_exp = left_exp
|
17
|
+
@cases = cases
|
18
|
+
@id = id
|
19
|
+
end
|
20
|
+
|
21
|
+
def comp
|
22
|
+
[@type_str, @left_exp, @cases]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Note that the expression this returns is not wrapped by ().
|
26
|
+
def to_low(set, env)
|
27
|
+
check_completeness(set)
|
28
|
+
tmp_var_str = env.new_var(@left_exp.type_str)
|
29
|
+
left_let_exp = "#{tmp_var_str} = #{@left_exp.to_low(set, env)}"
|
30
|
+
case_exp = L.if_chain_exp do |i|
|
31
|
+
@cases.each do |c|
|
32
|
+
cond_exps = c.pattern.low_cond_exps(set, tmp_var_str)
|
33
|
+
let_exps = c.pattern.low_let_exps(set, tmp_var_str, env)
|
34
|
+
exp = (let_exps + [c.exp.to_low(set, env)]).join(', ')
|
35
|
+
i.finish(exp) if cond_exps.empty?
|
36
|
+
i.append_case(cond_exps.join(' && '), exp)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
"#{left_let_exp}, #{case_exp}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_completeness(set)
|
43
|
+
set.type(@left_exp.type_str).all_pattern_examples(set).each do |exam|
|
44
|
+
unless @cases.any? { |c| c.pattern.accept?(exam) }
|
45
|
+
raise IncompleteMatchExpError.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def memory(set)
|
51
|
+
m = @cases.map { |c| c.exp.memory(set) }.reduce { |a, b| a.or(b) }
|
52
|
+
@left_exp.memory(set).and(m)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class FuncCallExp < Exp
|
57
|
+
def initialize(type_str, func_str, arg_exps, id = nil)
|
58
|
+
@type_str = type_str
|
59
|
+
@func_str = func_str
|
60
|
+
@arg_exps = arg_exps
|
61
|
+
@id = id
|
62
|
+
end
|
63
|
+
|
64
|
+
def comp
|
65
|
+
[@type_str, @func_str, @arg_exps]
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_low(set, env)
|
69
|
+
low_arg_exps = @arg_exps.map { |e| e.to_low(set, env) }
|
70
|
+
set.func(@func_str).low_call_exp_in_exp(set, env, low_arg_exps)
|
71
|
+
end
|
72
|
+
|
73
|
+
def memory(set)
|
74
|
+
@arg_exps.reduce(set.func(@func_str).memory(set)) do |m, e|
|
75
|
+
m.and(e.memory(set))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class VConstCallExp < Exp
|
81
|
+
def initialize(type_str, vconst_str, arg_exps, id = nil)
|
82
|
+
@type_str = type_str
|
83
|
+
@vconst_str = vconst_str
|
84
|
+
@arg_exps = arg_exps
|
85
|
+
@id = id
|
86
|
+
end
|
87
|
+
|
88
|
+
def comp
|
89
|
+
[@type_str, @vconst_strs, @arg_exps]
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_low(set, env)
|
93
|
+
low_arg_exps = @arg_exps.map { |e| e.to_low(set, env) }
|
94
|
+
set.vconst(@vconst_str).low_constructor_call_exp(low_arg_exps)
|
95
|
+
end
|
96
|
+
|
97
|
+
def memory(set)
|
98
|
+
@arg_exps.reduce(Memory.one(@type_str)) do |m, e|
|
99
|
+
m.and(e.memory(set))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class VarRefExp < Exp
|
105
|
+
def initialize(type_str, var_str, id = nil)
|
106
|
+
@type_str = type_str
|
107
|
+
@var_str = var_str
|
108
|
+
@id = id
|
109
|
+
end
|
110
|
+
|
111
|
+
def comp
|
112
|
+
[@type_str, @var_str]
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_low(_set, _env)
|
116
|
+
@var_str
|
117
|
+
end
|
118
|
+
|
119
|
+
def memory(_set)
|
120
|
+
Memory.empty
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'sfrp/mono/exception'
|
2
|
+
|
3
|
+
module SFRP
|
4
|
+
module Mono
|
5
|
+
class Function
|
6
|
+
FType = Struct.new(:param_type_strs, :return_type_str)
|
7
|
+
|
8
|
+
attr_reader :str
|
9
|
+
|
10
|
+
def initialize(str, param_strs, ftype, exp = nil, ffi_str = nil)
|
11
|
+
raise ArgumentError if exp.nil? && ffi_str.nil?
|
12
|
+
raise ArgumentError unless param_strs.size == ftype.param_type_strs.size
|
13
|
+
@str = str
|
14
|
+
@param_strs = param_strs
|
15
|
+
@ftype = ftype
|
16
|
+
@exp = exp
|
17
|
+
@ffi_str = ffi_str
|
18
|
+
end
|
19
|
+
|
20
|
+
def comp
|
21
|
+
[@str, @param_strs, @ftype, @exp, @ffi_str]
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
comp == other.comp
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return max needed memory size to call this function once.
|
29
|
+
# If this func is a foreign function, this size is assumed as max needed
|
30
|
+
# memory size to hold return-type.
|
31
|
+
def memory(set)
|
32
|
+
return set.type(@ftype.return_type_str).memory(set) if @ffi_str
|
33
|
+
@exp.memory(set)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return low-expression to call this function.
|
37
|
+
def low_call_exp(low_arg_exps)
|
38
|
+
return L.call_exp(@ffi_str, low_arg_exps) if @ffi_str
|
39
|
+
L.call_exp(@str, low_arg_exps)
|
40
|
+
end
|
41
|
+
|
42
|
+
def low_call_exp_in_exp(set, env, low_arg_exps)
|
43
|
+
if @ffi_str
|
44
|
+
type = set.type(@ftype.return_type_str)
|
45
|
+
if @ffi_str !~ /[a-zA-Z]/
|
46
|
+
if low_arg_exps.size == 2
|
47
|
+
return "(#{low_arg_exps[0]}) #{@ffi_str} (#{low_arg_exps[1]})"
|
48
|
+
end
|
49
|
+
if low_arg_exps.size == 1
|
50
|
+
return "#{@ffi_str} (#{low_arg_exps[0]})"
|
51
|
+
end
|
52
|
+
raise InvalidTypeOfForeignFunctionError.new(@ffi_str)
|
53
|
+
end
|
54
|
+
if type.native?
|
55
|
+
return L.call_exp(@ffi_str, low_arg_exps)
|
56
|
+
end
|
57
|
+
if type.linear?(set)
|
58
|
+
var = env.new_var(@ftype.return_type_str)
|
59
|
+
pointers = type.low_member_pointers_for_single_vconst(set, var)
|
60
|
+
call_exp = L.call_exp(@ffi_str, low_arg_exps + pointers)
|
61
|
+
return "(#{var} = #{type.low_allocator_str}(0), #{call_exp}, #{var})"
|
62
|
+
end
|
63
|
+
raise InvalidTypeOfForeignFunctionError.new(@ffi_str)
|
64
|
+
end
|
65
|
+
L.call_exp(@str, low_arg_exps)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate function in C for this function.
|
69
|
+
def gen(src_set, dest_set)
|
70
|
+
return if @ffi_str
|
71
|
+
env = Environment.new
|
72
|
+
type = src_set.type(@ftype.return_type_str)
|
73
|
+
dest_set << L.function(@str, type.low_type_str) do |f|
|
74
|
+
@param_strs.zip(@ftype.param_type_strs).map do |p_str, t_str|
|
75
|
+
f.append_param(src_set.type(t_str).low_type_str, p_str)
|
76
|
+
end
|
77
|
+
stmt = L.stmt("return #{@exp.to_low(src_set, env)}")
|
78
|
+
env.each_declared_vars do |var_str, type_str|
|
79
|
+
f << L.stmt("#{src_set.type(type_str).low_type_str} #{var_str}")
|
80
|
+
end
|
81
|
+
f << stmt
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SFRP
|
2
|
+
module Mono
|
3
|
+
class Memory
|
4
|
+
def self.empty
|
5
|
+
Memory.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.one(type_str)
|
9
|
+
Memory.new(type_str => 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(hash = {})
|
13
|
+
@hash = hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def and(other)
|
17
|
+
Memory.new(@hash.merge(other.hash) { |_, v1, v2| v1 + v2 })
|
18
|
+
end
|
19
|
+
|
20
|
+
def or(other)
|
21
|
+
Memory.new(@hash.merge(other.hash) { |_, v1, v2| [v1, v2].max })
|
22
|
+
end
|
23
|
+
|
24
|
+
def count(type_str)
|
25
|
+
@hash[type_str] || 0
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
attr_reader :hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|