skeem 0.1.03 → 0.2.00
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/lib/skeem/grammar.rb +4 -2
- data/lib/skeem/interpreter.rb +24 -9
- data/lib/skeem/primitive/primitive_builder.rb +18 -18
- data/lib/skeem/primitive/primitive_procedure.rb +32 -6
- data/lib/skeem/runtime.rb +34 -32
- data/lib/skeem/s_expr_builder.rb +9 -3
- data/lib/skeem/s_expr_nodes.rb +203 -188
- data/lib/skeem/skm_binding.rb +103 -0
- data/lib/skeem/skm_element.rb +20 -10
- data/lib/skeem/skm_frame.rb +137 -0
- data/lib/skeem/skm_pair.rb +2 -2
- data/lib/skeem/skm_procedure_exec.rb +31 -0
- data/lib/skeem/standard/base.skm +15 -1
- data/lib/skeem/tokenizer.rb +2 -1
- data/lib/skeem/version.rb +1 -1
- data/spec/skeem/interpreter_spec.rb +30 -16
- data/spec/skeem/lambda_spec.rb +114 -0
- data/spec/skeem/primitive/primitive_builder_spec.rb +16 -12
- data/spec/skeem/primitive/primitive_procedure_spec.rb +14 -21
- data/spec/skeem/runtime_spec.rb +14 -12
- data/spec/skeem/s_expr_nodes_spec.rb +5 -81
- data/spec/skeem/skm_frame_spec.rb +119 -0
- data/spec/skeem/skm_procedure_exec_spec.rb +63 -0
- data/spec/skeem/skm_unary_expression_spec.rb +2 -3
- metadata +11 -2
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative 'skm_element'
|
2
|
+
|
3
|
+
module Skeem
|
4
|
+
|
5
|
+
# An identifier that is not a syntactic keyword can be used as a variable.
|
6
|
+
# A variable may give a name to value that is bound (i.e. associated)
|
7
|
+
# to that variable.
|
8
|
+
class SkmBinding < SkmElement
|
9
|
+
# @return [SkmIdentifier] The identifier that is bound the value.
|
10
|
+
attr_reader(:variable)
|
11
|
+
|
12
|
+
# @return [SkmElement] The Skeem object that is associated with the variable.
|
13
|
+
attr_reader(:value)
|
14
|
+
|
15
|
+
# Constructor
|
16
|
+
# @param anIdentifier [SkmIdentifier] The variable name
|
17
|
+
# @param aValue [SkmElement] The value to bind to the variable.
|
18
|
+
def initialize(anIdentifier, aValue)
|
19
|
+
@variable = anIdentifier
|
20
|
+
@value = aValue
|
21
|
+
end
|
22
|
+
|
23
|
+
def evaluate(aRuntime)
|
24
|
+
name = variable.evaluate(aRuntime)
|
25
|
+
|
26
|
+
if value.kind_of?(SkmVariableReference)
|
27
|
+
other_name = value.variable.evaluate(aRuntime)
|
28
|
+
if name.value != other_name.value
|
29
|
+
entry = aRuntime.fetch(other_name)
|
30
|
+
result = value.evaluate(aRuntime)
|
31
|
+
if entry.callable?
|
32
|
+
@value = entry
|
33
|
+
end
|
34
|
+
else
|
35
|
+
# same name in definiens
|
36
|
+
raise StandardError
|
37
|
+
end
|
38
|
+
else
|
39
|
+
result = value.evaluate(aRuntime)
|
40
|
+
end
|
41
|
+
|
42
|
+
=begin
|
43
|
+
aRuntime.add_binding(var_key, self)
|
44
|
+
case expression
|
45
|
+
when SkmLambda
|
46
|
+
result = expression.evaluate(aRuntime)
|
47
|
+
|
48
|
+
when SkmVariableReference
|
49
|
+
other_key = expression.variable.evaluate(aRuntime)
|
50
|
+
if var_key.value != other_key.value
|
51
|
+
entry = aRuntime.fetch(other_key)
|
52
|
+
result = expression.evaluate(aRuntime)
|
53
|
+
if entry.kind_of?(Primitive::PrimitiveProcedure)
|
54
|
+
@expression = entry
|
55
|
+
elsif entry.kind_of?(SkmDefinition)
|
56
|
+
if entry.expression.kind_of?(SkmLambda)
|
57
|
+
@expression = entry.expression
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
# INFINITE LOOP DANGER: definition of 'x' has a reference to 'x'!
|
62
|
+
# Way out: the lookup for the reference should start from outer
|
63
|
+
# environment.
|
64
|
+
env = aRuntime.pop
|
65
|
+
@expression = expression.evaluate(aRuntime)
|
66
|
+
aRuntime.push(env)
|
67
|
+
result = expression
|
68
|
+
end
|
69
|
+
else
|
70
|
+
result = self
|
71
|
+
end
|
72
|
+
=end
|
73
|
+
binding_action(aRuntime, name, result)
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def binding_action(aRuntime, anIdentifier, anExpression)
|
80
|
+
aRuntime.add_binding(anIdentifier, anExpression)
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect_specific
|
84
|
+
result = variable.inspect
|
85
|
+
if value
|
86
|
+
result << ', ' << value.inspect
|
87
|
+
end
|
88
|
+
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
end # class
|
93
|
+
|
94
|
+
|
95
|
+
class SkmUpdateBinding < SkmBinding
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def binding_action(aRuntime, anIdentifier, anExpression)
|
100
|
+
aRuntime.update_binding(anIdentifier, anExpression)
|
101
|
+
end
|
102
|
+
end # class
|
103
|
+
end # module
|
data/lib/skeem/skm_element.rb
CHANGED
@@ -4,6 +4,10 @@ module Skeem
|
|
4
4
|
def initialize(aPosition)
|
5
5
|
self.position = aPosition
|
6
6
|
end
|
7
|
+
|
8
|
+
def callable?
|
9
|
+
false
|
10
|
+
end
|
7
11
|
|
8
12
|
def number?
|
9
13
|
false
|
@@ -36,7 +40,7 @@ module Skeem
|
|
36
40
|
def null?
|
37
41
|
false
|
38
42
|
end
|
39
|
-
|
43
|
+
|
40
44
|
def pair?
|
41
45
|
false
|
42
46
|
end
|
@@ -44,16 +48,16 @@ module Skeem
|
|
44
48
|
def vector?
|
45
49
|
false
|
46
50
|
end
|
47
|
-
|
51
|
+
|
48
52
|
def eqv?(other)
|
49
53
|
equal?(other)
|
50
54
|
end
|
51
|
-
|
55
|
+
|
52
56
|
def skm_equal?(_other)
|
53
57
|
msg = "Missing implementation of method #{self.class.name}##{__method__}"
|
54
58
|
raise NotImplementedError, msg
|
55
59
|
end
|
56
|
-
|
60
|
+
|
57
61
|
# @return [TrueClass, FalseClass] true if quoted element is identical to itself
|
58
62
|
def verbatim?
|
59
63
|
false
|
@@ -62,10 +66,10 @@ module Skeem
|
|
62
66
|
def evaluate(_runtime)
|
63
67
|
raise NotImplementedError, "Missing implementation of #{self.class.name}"
|
64
68
|
end
|
65
|
-
|
69
|
+
|
66
70
|
def quasiquote(_runtime)
|
67
71
|
raise NotImplementedError, "Missing implementation of #{self.class.name}"
|
68
|
-
end
|
72
|
+
end
|
69
73
|
|
70
74
|
# Abstract method.
|
71
75
|
# Part of the 'visitee' role in Visitor design pattern.
|
@@ -77,14 +81,20 @@ module Skeem
|
|
77
81
|
def done!
|
78
82
|
# Do nothing
|
79
83
|
end
|
80
|
-
|
84
|
+
|
81
85
|
def quoted!
|
82
86
|
# Do nothing
|
83
87
|
end
|
84
|
-
|
88
|
+
|
85
89
|
def unquoted!
|
86
90
|
# Do nothing
|
87
|
-
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Notification that this procedure is bound to a variable
|
94
|
+
# @param [Skemm::SkmFrame]
|
95
|
+
def bound!(_frame)
|
96
|
+
# Do nothing
|
97
|
+
end
|
88
98
|
|
89
99
|
def inspect
|
90
100
|
result = inspect_prefix
|
@@ -105,7 +115,7 @@ module Skeem
|
|
105
115
|
end
|
106
116
|
|
107
117
|
def inspect_specific
|
108
|
-
raise NotImplementedError
|
118
|
+
raise NotImplementedError, "Missing #{self.class.to_s + '#' + 'inspect_specific'}"
|
109
119
|
end
|
110
120
|
end # struct
|
111
121
|
end # module
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative 'skm_simple_datum'
|
2
|
+
|
3
|
+
module Skeem
|
4
|
+
# A frame is a set of bindings of the form { String => SkmElement }.
|
5
|
+
# It associate to each identifier (as String) a Skeem object.
|
6
|
+
class SkmFrame
|
7
|
+
# @return [SkmFrame, nil] Link to parent frame (if any).
|
8
|
+
attr_reader :parent
|
9
|
+
|
10
|
+
# @return [Hash{String => SkmElement}] Map of variable names => values.
|
11
|
+
attr_reader(:bindings)
|
12
|
+
|
13
|
+
# Constructor
|
14
|
+
# @param parentFrame[SkmFrame] Parent frame of this one.
|
15
|
+
def initialize(parentFrame = nil)
|
16
|
+
@bindings = {}
|
17
|
+
@parent = parentFrame
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a binding to this frame.
|
21
|
+
# There is no check that the variable name is already in use.
|
22
|
+
# @param anIdentifier [String, SkmIdentifier] The variable name
|
23
|
+
# @param anExpression [SkmElement] The Skeem expression to bind
|
24
|
+
def add_binding(anIdentifier, anExpression)
|
25
|
+
bind(valid_identifier(anIdentifier), anExpression)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Update the value of an existing variable.
|
29
|
+
# @param anIdentifier [String, SkmIdentifier] The variable name.
|
30
|
+
# @param anExpression [SkmElement] The Skeem expression to bind
|
31
|
+
def update_binding(anIdentifier, anExpression)
|
32
|
+
variable = valid_identifier(anIdentifier)
|
33
|
+
if bindings.include?(variable)
|
34
|
+
bind(variable, anExpression)
|
35
|
+
elsif parent
|
36
|
+
parent.update_binding(variable, anExpression)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieve the value of an existing variable.
|
41
|
+
# @param anIdentifier [String, SkmIdentifier] The variable name.
|
42
|
+
# @return [SkmElement]
|
43
|
+
def fetch(anIdentifier)
|
44
|
+
variable = valid_identifier(anIdentifier)
|
45
|
+
found = bindings[variable]
|
46
|
+
if found.nil? && parent
|
47
|
+
found = parent.fetch(variable)
|
48
|
+
end
|
49
|
+
|
50
|
+
found
|
51
|
+
end
|
52
|
+
|
53
|
+
# Tell the value of an existing variable.
|
54
|
+
# @param anIdentifier [String, SkmIdentifier] The variable name.
|
55
|
+
# @return [Boolean]
|
56
|
+
def include?(anIdentifier)
|
57
|
+
variable = valid_identifier(anIdentifier)
|
58
|
+
my_result = bindings.include?(variable)
|
59
|
+
if my_result == false && parent
|
60
|
+
my_result = parent.include?(variable)
|
61
|
+
end
|
62
|
+
|
63
|
+
my_result
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Boolean]
|
67
|
+
def empty?
|
68
|
+
my_result = bindings.empty?
|
69
|
+
if my_result && parent
|
70
|
+
my_result = parent.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
my_result
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer]
|
77
|
+
def size
|
78
|
+
my_result = bindings.size
|
79
|
+
my_result += parent.size if parent
|
80
|
+
|
81
|
+
my_result
|
82
|
+
end
|
83
|
+
|
84
|
+
# The number of parents this frame has.
|
85
|
+
# @return [Integer] The nesting levels
|
86
|
+
def depth
|
87
|
+
count = 0
|
88
|
+
|
89
|
+
curr_frame = self
|
90
|
+
while curr_frame.parent
|
91
|
+
count += 1
|
92
|
+
curr_frame = curr_frame.parent
|
93
|
+
end
|
94
|
+
|
95
|
+
count
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def inspect
|
100
|
+
result = ''
|
101
|
+
if parent
|
102
|
+
result << parent.inspect
|
103
|
+
else
|
104
|
+
return "\n"
|
105
|
+
end
|
106
|
+
result << "\n----\n"
|
107
|
+
bindings.each_pair do |key, expr|
|
108
|
+
result << "#{key.inspect} => #{expr.inspect}\n"
|
109
|
+
end
|
110
|
+
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def valid_identifier(anIdentifier)
|
117
|
+
case anIdentifier
|
118
|
+
when String
|
119
|
+
anIdentifier
|
120
|
+
when SkmIdentifier
|
121
|
+
anIdentifier.value
|
122
|
+
else
|
123
|
+
klass = anIdentifier.class.to_s
|
124
|
+
err_msg = "Invalid identifier: #{anIdentifier} has type #{klass}."
|
125
|
+
raise StandardError, err_msg
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def bind(anIdentifier, anExpression)
|
130
|
+
@bindings[anIdentifier] = anExpression
|
131
|
+
|
132
|
+
# Notify the value that it is bound to a variable from this frame.
|
133
|
+
anExpression.bound!(self)
|
134
|
+
end
|
135
|
+
|
136
|
+
end # class
|
137
|
+
end # module
|
data/lib/skeem/skm_pair.rb
CHANGED
@@ -133,8 +133,8 @@ module Skeem
|
|
133
133
|
begin
|
134
134
|
result = clone_evaluate(aRuntime)
|
135
135
|
rescue NoMethodError => exc
|
136
|
-
$stderr.puts self.inspect
|
137
|
-
$stderr.puts self.to_a.inspect
|
136
|
+
$stderr.puts 'SkmPair#evaluate: ' + self.inspect
|
137
|
+
$stderr.puts 'SkmPair as Array: ' + self.to_a.inspect
|
138
138
|
raise exc
|
139
139
|
end
|
140
140
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'runtime'
|
2
|
+
|
3
|
+
module Skeem
|
4
|
+
class SkmProcedureExec
|
5
|
+
attr_reader :frame
|
6
|
+
attr_reader :definition
|
7
|
+
|
8
|
+
def initialize(aLambda)
|
9
|
+
@definition = aLambda
|
10
|
+
@frame = SkmFrame.new(definition.environment)
|
11
|
+
# $stderr.puts "New SkmProcedureExec"
|
12
|
+
# $stderr.puts " frame = #{frame.object_id.to_s(16)}"
|
13
|
+
# $stderr.puts " Lambda = #{aLambda.object_id.to_s(16)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param theActuals [Array<SkmElement>]
|
17
|
+
def run!(aRuntime, theActuals)
|
18
|
+
runtime = aRuntime
|
19
|
+
runtime.push(frame)
|
20
|
+
definition.bind_locals(runtime, theActuals)
|
21
|
+
# $stderr.puts "Locals"
|
22
|
+
# $stderr.puts frame.bindings.keys.join(', ')
|
23
|
+
definition.evaluate_defs(runtime)
|
24
|
+
result = definition.evaluate_sequence(runtime)
|
25
|
+
runtime.pop
|
26
|
+
# $stderr.puts "Lambda result: #{result.object_id.to_s(16)}" if result.kind_of?(SkmLambda)
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end # class
|
31
|
+
end # module
|
data/lib/skeem/standard/base.skm
CHANGED
@@ -98,4 +98,18 @@
|
|
98
98
|
; (car ls)
|
99
99
|
; (list-ref (cdr ls) (- n 1)))))
|
100
100
|
|
101
|
-
(define symbol=? string=?)
|
101
|
+
(define symbol=? string=?)
|
102
|
+
|
103
|
+
;; Test the equivalence (with eqv? predicate) between an expected value and
|
104
|
+
;; an expression
|
105
|
+
;; (test-eqv expected test-expr)
|
106
|
+
(define test-eqv
|
107
|
+
(lambda (x y)
|
108
|
+
(test-assert (equal? x y))))
|
109
|
+
|
110
|
+
;; Test the equality (with equal? predicate) between an expected value and
|
111
|
+
;; an expression
|
112
|
+
;; (test-equal expected test-expr)
|
113
|
+
(define test-equal
|
114
|
+
(lambda (x y)
|
115
|
+
(test-assert (equal? x y))))
|
data/lib/skeem/tokenizer.rb
CHANGED
@@ -29,7 +29,7 @@ module Skeem
|
|
29
29
|
'#(' => 'VECTOR_BEGIN'
|
30
30
|
}.freeze
|
31
31
|
|
32
|
-
# Here are all the
|
32
|
+
# Here are all the Scheme keywords (in uppercase)
|
33
33
|
@@keywords = %w[
|
34
34
|
BEGIN
|
35
35
|
DEFINE
|
@@ -37,6 +37,7 @@ module Skeem
|
|
37
37
|
LAMBDA
|
38
38
|
QUASIQUOTE
|
39
39
|
QUOTE
|
40
|
+
SET!
|
40
41
|
UNQUOTE
|
41
42
|
UNQUOTE-SPLICING
|
42
43
|
].map { |x| [x, x] } .to_h
|
data/lib/skeem/version.rb
CHANGED
@@ -8,8 +8,12 @@ module Skeem
|
|
8
8
|
include DatumDSL
|
9
9
|
|
10
10
|
context 'Initialization:' do
|
11
|
-
it '
|
12
|
-
expect { Interpreter.new
|
11
|
+
it 'could be initialized without an argument' do
|
12
|
+
expect { Interpreter.new }.not_to raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'could be initialized with a block argument' do
|
16
|
+
expect { Interpreter.new { |interp| } }.not_to raise_error
|
13
17
|
end
|
14
18
|
|
15
19
|
it 'should have a parser' do
|
@@ -26,8 +30,9 @@ module Skeem
|
|
26
30
|
|
27
31
|
it 'should implement base bindings' do
|
28
32
|
expect(subject.fetch('number?')).to be_kind_of(Primitive::PrimitiveProcedure)
|
29
|
-
expect(subject.fetch('abs')).to be_kind_of(
|
30
|
-
expect(subject.fetch('abs').
|
33
|
+
expect(subject.fetch('abs')).to be_kind_of(SkmLambda)
|
34
|
+
expect(subject.fetch('abs').formals.arity).to eq(1)
|
35
|
+
expect(subject.fetch('abs').formals.formals[0]).to eq('x')
|
31
36
|
end
|
32
37
|
end # context
|
33
38
|
|
@@ -107,7 +112,7 @@ module Skeem
|
|
107
112
|
context 'Built-in primitives' do
|
108
113
|
it 'should implement variable definition' do
|
109
114
|
result = subject.run('(define x 28)')
|
110
|
-
expect(
|
115
|
+
expect(subject.fetch('x')).to eq(28)
|
111
116
|
end
|
112
117
|
|
113
118
|
it 'should implement variable reference' do
|
@@ -125,7 +130,7 @@ SKEEM
|
|
125
130
|
it 'should implement the simple conditional form' do
|
126
131
|
checks = [
|
127
132
|
['(if (> 3 2) "yes")', 'yes'],
|
128
|
-
['(if (> 2 3) "yes")',
|
133
|
+
['(if (> 2 3) "yes")', SkmUndefined.instance]
|
129
134
|
]
|
130
135
|
checks.each do |(skeem_expr, expectation)|
|
131
136
|
result = subject.run(skeem_expr)
|
@@ -213,7 +218,7 @@ SKEEM
|
|
213
218
|
(if (< x 0) (- x) x)))
|
214
219
|
SKEEM
|
215
220
|
subject.run(source)
|
216
|
-
procedure = subject.fetch('abs')
|
221
|
+
procedure = subject.fetch('abs')
|
217
222
|
expect(procedure.arity).to eq(1)
|
218
223
|
result = subject.run('(abs -3)')
|
219
224
|
expect(result).to eq(3)
|
@@ -231,7 +236,7 @@ SKEEM
|
|
231
236
|
(if (< x y) x y)))
|
232
237
|
SKEEM
|
233
238
|
subject.run(source)
|
234
|
-
procedure = subject.fetch('min')
|
239
|
+
procedure = subject.fetch('min')
|
235
240
|
expect(procedure.arity).to eq(2)
|
236
241
|
result = subject.run('(min 1 2)')
|
237
242
|
expect(result).to eq(1)
|
@@ -631,7 +636,7 @@ SKEEM
|
|
631
636
|
expect(result.to_a).to eq(expectation)
|
632
637
|
end
|
633
638
|
end
|
634
|
-
|
639
|
+
|
635
640
|
it 'should implement the cddr procedure' do
|
636
641
|
checks = [
|
637
642
|
["(cddr '(2 1))", []]
|
@@ -640,7 +645,7 @@ SKEEM
|
|
640
645
|
result = subject.run(skeem_expr)
|
641
646
|
expect(result.to_a).to eq(expectation)
|
642
647
|
end
|
643
|
-
end
|
648
|
+
end
|
644
649
|
|
645
650
|
it 'should implement the symbol=? procedure' do
|
646
651
|
checks = [
|
@@ -656,19 +661,27 @@ SKEEM
|
|
656
661
|
end # context
|
657
662
|
|
658
663
|
context 'More advanced tests' do
|
659
|
-
it 'should implement second-order functions' do
|
664
|
+
it 'should implement lambda that calls second-order functions' do
|
660
665
|
source = <<-SKEEM
|
666
|
+
(define twice
|
667
|
+
(lambda (x)
|
668
|
+
(* 2 x)))
|
661
669
|
(define compose
|
662
670
|
(lambda (f g)
|
663
671
|
(lambda (x)
|
664
672
|
(f (g x)))))
|
665
|
-
(
|
673
|
+
(define repeat
|
674
|
+
(lambda (f)
|
675
|
+
(compose f f)))
|
676
|
+
((repeat twice) 5)
|
666
677
|
SKEEM
|
667
678
|
result = subject.run(source)
|
668
|
-
expect(result.last
|
679
|
+
expect(result.last).to eq(20)
|
669
680
|
end
|
670
681
|
|
671
|
-
|
682
|
+
#=begin
|
683
|
+
it 'under construction' do
|
684
|
+
# Fails
|
672
685
|
source = <<-SKEEM
|
673
686
|
(define twice
|
674
687
|
(lambda (x)
|
@@ -680,11 +693,12 @@ SKEEM
|
|
680
693
|
(define repeat
|
681
694
|
(lambda (f)
|
682
695
|
(compose f f)))
|
683
|
-
((repeat twice) 5)
|
696
|
+
((repeat (repeat twice)) 5)
|
684
697
|
SKEEM
|
685
698
|
result = subject.run(source)
|
686
|
-
expect(result.last).to eq(
|
699
|
+
expect(result.last).to eq(80)
|
687
700
|
end
|
701
|
+
#=end
|
688
702
|
end # context
|
689
703
|
end # describe
|
690
704
|
end # module
|