sexpr 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +24 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/LICENCE.md +1 -1
- data/Manifest.txt +1 -0
- data/README.md +46 -31
- data/examples/bool_expr/bool_expr.citrus +72 -0
- data/examples/bool_expr/bool_expr.rb +86 -0
- data/examples/bool_expr/bool_expr.sexp.yml +23 -0
- data/lib/sexpr.rb +27 -20
- data/lib/sexpr/errors.rb +15 -0
- data/lib/sexpr/grammar.rb +21 -77
- data/lib/sexpr/grammar/matching.rb +56 -0
- data/lib/sexpr/grammar/options.rb +53 -0
- data/lib/sexpr/grammar/parsing.rb +20 -0
- data/lib/sexpr/grammar/tagging.rb +49 -0
- data/lib/sexpr/loader.rb +0 -1
- data/lib/sexpr/matcher.rb +15 -0
- data/lib/sexpr/matcher/alternative.rb +30 -0
- data/lib/sexpr/matcher/many.rb +54 -0
- data/lib/sexpr/matcher/reference.rb +32 -0
- data/lib/sexpr/matcher/rule.rb +31 -0
- data/lib/sexpr/matcher/sequence.rb +30 -0
- data/lib/sexpr/matcher/terminal.rb +37 -0
- data/lib/sexpr/node.rb +16 -0
- data/lib/sexpr/parser.rb +48 -0
- data/lib/sexpr/parser/citrus.rb +67 -0
- data/lib/sexpr/version.rb +2 -2
- data/sexpr.gemspec +1 -0
- data/sexpr.noespec +2 -1
- data/spec/grammar/matching/test_compile_rule.rb +23 -0
- data/spec/grammar/matching/test_compile_rule_defn.rb +103 -0
- data/spec/grammar/options/test_install_parser.rb +36 -0
- data/spec/grammar/options/test_install_path.rb +19 -0
- data/spec/grammar/options/test_install_root.rb +27 -0
- data/spec/grammar/tagging/test_looks_a_sexpr.rb +20 -0
- data/spec/grammar/tagging/test_mod2rulename.rb +19 -0
- data/spec/grammar/tagging/test_rule2modname.rb +19 -0
- data/spec/grammar/tagging/test_tag_sexpr.rb +28 -0
- data/spec/grammar/test_new.rb +15 -0
- data/spec/grammar/test_parse.rb +27 -18
- data/spec/grammar/test_sexpr.rb +53 -0
- data/spec/{alternative → matcher/alternative}/test_eat.rb +1 -1
- data/spec/{alternative → matcher/alternative}/test_match_q.rb +1 -1
- data/spec/{many → matcher/many}/test_eat.rb +1 -1
- data/spec/{many → matcher/many}/test_initialize.rb +1 -1
- data/spec/{many → matcher/many}/test_match_q.rb +1 -1
- data/spec/{reference → matcher/reference}/test_eat.rb +1 -1
- data/spec/{reference → matcher/reference}/test_match_q.rb +1 -1
- data/spec/{rule → matcher/rule}/test_eat.rb +1 -1
- data/spec/{rule → matcher/rule}/test_match_q.rb +1 -1
- data/spec/{sequence → matcher/sequence}/test_eat.rb +1 -1
- data/spec/{sequence → matcher/sequence}/test_match_q.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_eat.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_match_q.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_terminal_match.rb +1 -1
- data/spec/node/test_sexpr_body.rb +18 -0
- data/spec/node/test_sexpr_type.rb +14 -0
- data/spec/parser/citrus/test_new.rb +28 -0
- data/spec/parser/citrus/test_parse.rb +40 -0
- data/spec/parser/citrus/test_recognize.rb +18 -0
- data/spec/parser/citrus/test_registration.rb +20 -0
- data/spec/parser/citrus/test_to_sexpr.rb +16 -0
- data/spec/parser/test_factor.rb +17 -0
- data/spec/parser/test_input_text.rb +27 -0
- data/spec/spec_helper.rb +18 -1
- data/spec/test_load.rb +40 -13
- data/spec/test_readme_examples.rb +40 -30
- data/spec/test_sexpr.rb +1 -1
- metadata +118 -68
- data/lib/sexpr/alternative.rb +0 -28
- data/lib/sexpr/element.rb +0 -9
- data/lib/sexpr/many.rb +0 -52
- data/lib/sexpr/reference.rb +0 -30
- data/lib/sexpr/rule.rb +0 -29
- data/lib/sexpr/sequence.rb +0 -28
- data/lib/sexpr/terminal.rb +0 -35
- data/spec/bool_expr.yml +0 -19
- data/spec/grammar/test_compile_rule.rb +0 -25
- data/spec/grammar/test_compile_rule_defn.rb +0 -98
- data/spec/grammar/test_fetch.rb +0 -18
- data/spec/grammar/test_root.rb +0 -20
- data/spec/test_bool_expr.rb +0 -27
@@ -0,0 +1,67 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Parser
|
3
|
+
class Citrus
|
4
|
+
include Parser
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def recognizes?(arg)
|
9
|
+
looks_a_citrus_grammar?(arg) or
|
10
|
+
looks_a_citrus_file?(arg)
|
11
|
+
end
|
12
|
+
|
13
|
+
def looks_a_citrus_file?(arg)
|
14
|
+
arg = arg.to_path if arg.respond_to?(:to_path)
|
15
|
+
arg.is_a?(String) && File.exists?(arg) && (File.extname(arg) == ".citrus")
|
16
|
+
end
|
17
|
+
|
18
|
+
def looks_a_citrus_grammar?(arg)
|
19
|
+
defined?(::Citrus::Grammar) && arg.is_a?(Module) && arg.include?(::Citrus::Grammar)
|
20
|
+
end
|
21
|
+
|
22
|
+
end # class << self
|
23
|
+
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
def initialize(parser, options = {})
|
27
|
+
@parser = parser
|
28
|
+
@options = default_options.merge(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_options
|
32
|
+
{:from_match_to_sexpr => lambda{|match| match.value}}
|
33
|
+
end
|
34
|
+
|
35
|
+
def parser
|
36
|
+
@citrus_parser ||= begin
|
37
|
+
if self.class.looks_a_citrus_grammar?(@parser)
|
38
|
+
@parser
|
39
|
+
elsif self.class.looks_a_citrus_file?(@parser)
|
40
|
+
@parser = @parser.to_path if @parser.respond_to?(:to_path)
|
41
|
+
@parser = @parser[0...-(".citrus".length)]
|
42
|
+
::Citrus.load(@parser).last
|
43
|
+
else
|
44
|
+
raise UnrecognizedParserError, "Not a citrus parser #{@parser}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(input, options = {})
|
50
|
+
input = input_text(input)
|
51
|
+
parser.parse(input, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def sexpr(input, parse_options = {})
|
55
|
+
from_match_to_sexpr parse(input, parse_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def from_match_to_sexpr(match)
|
61
|
+
options[:from_match_to_sexpr].call(match)
|
62
|
+
end
|
63
|
+
|
64
|
+
Sexpr::Parser.register self
|
65
|
+
end # class Citrus
|
66
|
+
end # module Parser
|
67
|
+
end # module Sexpr
|
data/lib/sexpr/version.rb
CHANGED
data/sexpr.gemspec
CHANGED
@@ -124,6 +124,7 @@ Gem::Specification.new do |s|
|
|
124
124
|
# for each development dependency. These gems are required for developers
|
125
125
|
#
|
126
126
|
s.add_development_dependency("epath", "~> 0.0.1")
|
127
|
+
s.add_development_dependency("citrus", "~> 2.4")
|
127
128
|
s.add_development_dependency("rake", "~> 0.9.2")
|
128
129
|
s.add_development_dependency("rspec", "~> 2.8.0")
|
129
130
|
s.add_development_dependency("wlang", "~> 0.10.2")
|
data/sexpr.noespec
CHANGED
@@ -9,7 +9,7 @@ variables:
|
|
9
9
|
upper:
|
10
10
|
Sexpr
|
11
11
|
version:
|
12
|
-
0.
|
12
|
+
0.3.0
|
13
13
|
summary: |-
|
14
14
|
A compilation framework around s-expressions
|
15
15
|
description: |-
|
@@ -20,6 +20,7 @@ variables:
|
|
20
20
|
- https://github.com/blambeau/sexp
|
21
21
|
dependencies:
|
22
22
|
- {name: epath, version: "~> 0.0.1", groups: [development]}
|
23
|
+
- {name: citrus, version: "~> 2.4", groups: [development]}
|
23
24
|
- {name: rake, version: "~> 0.9.2", groups: [development]}
|
24
25
|
- {name: rspec, version: "~> 2.8.0", groups: [development]}
|
25
26
|
- {name: wlang, version: "~> 0.10.2", groups: [development]}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
module Grammar
|
4
|
+
describe Matching, "compile_rule" do
|
5
|
+
include Matching
|
6
|
+
|
7
|
+
it 'keep alternatives unchanged' do
|
8
|
+
compile_rule(:hello, Matcher::Alternative.new([]) ).should be_a(Matcher::Alternative)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'keep terminals unchanged' do
|
12
|
+
compile_rule(:hello, Matcher::Terminal.new(true) ).should be_a(Matcher::Terminal)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'keep creates a Rule englobing sequences' do
|
16
|
+
compiled = compile_rule(:hello, Matcher::Sequence.new([]) )
|
17
|
+
compiled.should be_a(Matcher::Rule)
|
18
|
+
compiled.name.should eq(:hello)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
module Grammar
|
4
|
+
describe Matching, "compile_rule_defn" do
|
5
|
+
include Matching
|
6
|
+
|
7
|
+
subject{ compile_rule_defn(arg, :grammar) }
|
8
|
+
|
9
|
+
def rules
|
10
|
+
{:a_rule => "hello"}
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'with an Element' do
|
14
|
+
let(:arg){ Matcher::Terminal.new(//) }
|
15
|
+
it 'returns arg itself' do
|
16
|
+
subject.should eq(arg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "with true" do
|
21
|
+
let(:arg){ true }
|
22
|
+
it 'gives it true' do
|
23
|
+
subject.should be_a(Matcher::Terminal)
|
24
|
+
subject.value.should eq(true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with false" do
|
29
|
+
let(:arg){ false }
|
30
|
+
it 'gives it false' do
|
31
|
+
subject.should be_a(Matcher::Terminal)
|
32
|
+
subject.value.should eq(false)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with nil" do
|
37
|
+
let(:arg){ nil }
|
38
|
+
it 'gives it nil' do
|
39
|
+
subject.should be_a(Matcher::Terminal)
|
40
|
+
subject.value.should eq(nil)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with an alternative array' do
|
45
|
+
let(:arg){ [true, false, nil] }
|
46
|
+
it 'factors an Alternative' do
|
47
|
+
subject.should be_a(Matcher::Alternative)
|
48
|
+
end
|
49
|
+
it 'compiles its elements' do
|
50
|
+
subject.terms.size.should eq(3)
|
51
|
+
subject.terms.all?{|x| x.is_a?(Matcher::Terminal)}.should be_true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with a sequence array' do
|
56
|
+
let(:arg){ [[true, false, nil]] }
|
57
|
+
it 'factors a Sequence' do
|
58
|
+
subject.should be_a(Matcher::Sequence)
|
59
|
+
end
|
60
|
+
it 'compiles its elements' do
|
61
|
+
subject.terms.size.should eq(3)
|
62
|
+
subject.terms.all?{|x| x.is_a?(Matcher::Terminal)}.should be_true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with subalternatives' do
|
67
|
+
let(:arg){ [ ["a_rule", [false, true, nil] ]] }
|
68
|
+
it 'compiles the last as an Alternative' do
|
69
|
+
subject.terms.last.should be_a(Matcher::Alternative)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'with a reference to a non-terminal' do
|
74
|
+
let(:arg){ "a_rule" }
|
75
|
+
it 'factors a Reference' do
|
76
|
+
subject.should be_a(Matcher::Reference)
|
77
|
+
end
|
78
|
+
it 'refers to the appropriate rule name' do
|
79
|
+
subject.rule_name.should eq(:a_rule)
|
80
|
+
end
|
81
|
+
it 'refers to the appropriate grammar' do
|
82
|
+
subject.grammar.should eq(:grammar)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'with a stared non-terminal' do
|
87
|
+
let(:arg){ "a_rule+" }
|
88
|
+
it 'factors a Many' do
|
89
|
+
subject.should be_a(Matcher::Many)
|
90
|
+
end
|
91
|
+
it 'refers to the appropriate rule' do
|
92
|
+
subject.term.should be_a(Matcher::Reference)
|
93
|
+
subject.term.rule_name.should eq(:a_rule)
|
94
|
+
end
|
95
|
+
it 'refers to the appropriate multiplicities' do
|
96
|
+
subject.min.should eq(1)
|
97
|
+
subject.max.should be_nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
describe Grammar::Options, "install_parser" do
|
4
|
+
include Grammar::Options
|
5
|
+
|
6
|
+
it 'is nil by default' do
|
7
|
+
h = {}
|
8
|
+
install_options h do
|
9
|
+
parser.should be_nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be specified through a :parser option' do
|
14
|
+
h = {:parser => bool_expr_parser}
|
15
|
+
install_options h do
|
16
|
+
parser.should be_a(Parser::Citrus)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can be specified as a Path' do
|
21
|
+
h = {:parser => fixtures_path/"bool_expr.citrus"}
|
22
|
+
install_options h do
|
23
|
+
parser.should be_a(Parser::Citrus)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can be specified as relative Path' do
|
28
|
+
h = {:path => fixtures_path/"bool_expr.sexp.yml",
|
29
|
+
:parser => "bool_expr.citrus"}
|
30
|
+
install_options h do
|
31
|
+
parser.should be_a(Parser::Citrus)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
describe Grammar::Options, "install_path" do
|
4
|
+
include Grammar::Options
|
5
|
+
|
6
|
+
it 'is nil by default' do
|
7
|
+
install_options({}) do
|
8
|
+
path.should be_nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'keeps the specified value if any' do
|
13
|
+
install_options :path => "blah" do
|
14
|
+
path.should eq("blah")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
describe Grammar::Options, "install_root" do
|
4
|
+
include Grammar::Options
|
5
|
+
|
6
|
+
let(:rules){ {:t => /[a-z]+/, :nt => true} }
|
7
|
+
|
8
|
+
it 'is the first key by default' do
|
9
|
+
install_options :rules => rules do
|
10
|
+
root.should eq(:t)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'is the specified rule when specified' do
|
15
|
+
install_options :rules => rules, :root => :nt do
|
16
|
+
root.should eq(:nt)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'is converted to a Symbol is a String' do
|
21
|
+
install_options :root => "nt" do
|
22
|
+
root.should eq(:nt)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr::Grammar
|
3
|
+
describe Tagging, "looks_a_sexpr" do
|
4
|
+
include Tagging
|
5
|
+
|
6
|
+
it 'recognizes s-expressions' do
|
7
|
+
looks_a_sexpr?([:lit]).should be_true
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'does not recognize empty arrays' do
|
11
|
+
looks_a_sexpr?([]).should be_false
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'does not recognize others' do
|
15
|
+
looks_a_sexpr?(nil).should be_false
|
16
|
+
looks_a_sexpr?(:lit).should be_false
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr::Grammar
|
3
|
+
describe Tagging, "mod2rulename" do
|
4
|
+
include Tagging
|
5
|
+
|
6
|
+
it 'work on simple module name' do
|
7
|
+
mod2rulename(:Test).should eq(:test)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'work on complex module name' do
|
11
|
+
mod2rulename(:ThisIsATest).should eq(:this_is_a_test)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'works with a module' do
|
15
|
+
mod2rulename(::Sexpr::Grammar).should eq(:grammar)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr::Grammar
|
3
|
+
describe Tagging, "rule2modname" do
|
4
|
+
include Tagging
|
5
|
+
|
6
|
+
it 'work on simple rule name' do
|
7
|
+
rule2modname(:test).should eq(:Test)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'works when underscores are present' do
|
11
|
+
rule2modname(:a_rule_name).should eq(:ARuleName)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'works with a string' do
|
15
|
+
rule2modname("a_rule_name").should eq(:ARuleName)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr::Grammar
|
3
|
+
describe Tagging, "tag_sexpr" do
|
4
|
+
include Tagging
|
5
|
+
|
6
|
+
module TaggingReference
|
7
|
+
module Not; end
|
8
|
+
module Lit; end
|
9
|
+
end
|
10
|
+
|
11
|
+
def tag(x)
|
12
|
+
res = tag_sexpr(x, TaggingReference)
|
13
|
+
res.should eq(x)
|
14
|
+
res
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'tags a sexpr at first level' do
|
18
|
+
tag([:lit]).should be_a(TaggingReference::Lit)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'tags sexpr recursively' do
|
22
|
+
res = tag([:not, [:lit, true]])
|
23
|
+
res.should be_a(TaggingReference::Not)
|
24
|
+
res.last.should be_a(TaggingReference::Lit)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Sexpr
|
3
|
+
describe Grammar, "new" do
|
4
|
+
|
5
|
+
subject{ Grammar.new }
|
6
|
+
|
7
|
+
it 'factors a module' do
|
8
|
+
subject.should be_a(Grammar)
|
9
|
+
subject.should be_a(Grammar::Options)
|
10
|
+
subject.should be_a(Grammar::Matching)
|
11
|
+
subject.should be_a(Grammar::Parsing)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
data/spec/grammar/test_parse.rb
CHANGED
@@ -2,31 +2,40 @@ require 'spec_helper'
|
|
2
2
|
module Sexpr
|
3
3
|
describe Grammar, "parse" do
|
4
4
|
|
5
|
-
def
|
6
|
-
|
7
|
-
def x.parse(s)
|
8
|
-
Struct.new(:value).new([:parsed, s])
|
9
|
-
end
|
10
|
-
}
|
5
|
+
def grammar
|
6
|
+
Sexpr.load(:parser => parser)
|
11
7
|
end
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
9
|
+
context 'when no parser is set' do
|
10
|
+
let(:parser){ nil }
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
|
12
|
+
it 'raises an error' do
|
13
|
+
lambda{
|
14
|
+
grammar.parse("Hello world")
|
15
|
+
}.should raise_error(NoParserError)
|
16
|
+
end
|
20
17
|
|
21
|
-
it 'it accepts a path' do
|
22
|
-
grammar.parse(Path.here).should eq([:parsed, File.read(__FILE__)])
|
23
18
|
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
context 'when a parser is set' do
|
21
|
+
let(:parser){
|
22
|
+
Object.new.extend Module.new{
|
23
|
+
include Parser
|
24
|
+
def parse(s, options = {})
|
25
|
+
[options[:root] || :parsed, input_text(s)]
|
26
|
+
end
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
it 'delegates the call to the parser' do
|
31
|
+
grammar.parse("Hello world").should eq([:parsed, "Hello world"])
|
28
32
|
end
|
29
|
-
|
33
|
+
|
34
|
+
it 'passes options' do
|
35
|
+
grammar.parse("world", :root => :hello).should eq([:hello, "world"])
|
36
|
+
end
|
37
|
+
|
38
|
+
end # when a parser is set
|
30
39
|
|
31
40
|
end
|
32
41
|
end
|