libfst 0.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/.yardopts +11 -0
- data/LICENSE +674 -0
- data/README.md +133 -0
- data/ext/extconf.rb +5 -0
- data/ext/fastlz.c +549 -0
- data/ext/fastlz.h +109 -0
- data/ext/fst_config.h +11 -0
- data/ext/fst_win_unistd.h +52 -0
- data/ext/fstapi.c +7204 -0
- data/ext/fstapi.h +466 -0
- data/ext/libfst_rb.c +2525 -0
- data/ext/lz4.c +2789 -0
- data/ext/lz4.h +868 -0
- data/lib/libfst/reader.rb +345 -0
- data/lib/libfst/tfp.rb +112 -0
- data/lib/libfst/vcd.rb +597 -0
- data/lib/libfst/version.rb +4 -0
- data/lib/libfst/writer.rb +50 -0
- data/lib/libfst.rb +6 -0
- data/libfst.gemspec +50 -0
- data/samples/create_file.rb +69 -0
- data/samples/gtkwave.png +0 -0
- data/samples/out.gtkw +46 -0
- data/samples/read2.rb +8 -0
- data/samples/read_file.rb +8 -0
- data/samples/skinny_rand.fst +0 -0
- data/samples/transaction_filter_process/full_boot.fst +0 -0
- data/samples/transaction_filter_process/full_boot.gtkw +39 -0
- data/samples/transaction_filter_process/sdcard.rb +793 -0
- data/samples/transaction_filter_process/uart.rb +141 -0
- data/samples/vcd/skinny_rand.vcd.xz +0 -0
- data/samples/vcd/vcd_read.rb +34 -0
- metadata +72 -0
@@ -0,0 +1,345 @@
|
|
1
|
+
# Copyright (C) 2024 Théotime Bollengier <theotime.bollengier@ensta-bretagne.fr>
|
2
|
+
#
|
3
|
+
# This file is part of libfst.rb <https://gitlab.ensta-bretagne.fr/bollenth/libfst.rb>
|
4
|
+
#
|
5
|
+
# libfst.rb is free software: you can redistribute it and/or modify it
|
6
|
+
# under the terms of the GNU General Public License as published
|
7
|
+
# by the Free Software Foundation, either version 3 of the License,
|
8
|
+
# or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# libfst.rb is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
13
|
+
# See the GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with libfst.rb. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
|
19
|
+
module LibFST
|
20
|
+
class Reader
|
21
|
+
# A Trace keeps track of the value changes of a physical signal.
|
22
|
+
class Trace
|
23
|
+
attr_reader :handle # @return [Integer]
|
24
|
+
attr_reader :variables # @return [Array<Variable>]
|
25
|
+
attr_accessor :is_float # @return [Boolean]
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def initialize(handle)
|
29
|
+
@handle = handle.to_i
|
30
|
+
@variables = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String]
|
34
|
+
def to_s
|
35
|
+
"<#{self.class.name} #{@handle} #{name} [#{@variables.collect(&:path).join(', ')}]>"
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [String]
|
39
|
+
def name
|
40
|
+
return @name if @name
|
41
|
+
update_name_and_path!
|
42
|
+
@name
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String]
|
46
|
+
def path
|
47
|
+
return @path if @path
|
48
|
+
update_name_and_path!
|
49
|
+
@path
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def update_name_and_path!
|
55
|
+
v = @variables.reject(&:is_alias)
|
56
|
+
raise "There are #{v.length} variables for the same trace which are not aliases!" if v.length != 1
|
57
|
+
v = v.first
|
58
|
+
@path = v.path
|
59
|
+
@name = v.name
|
60
|
+
self
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# An attribute can provide additional information to a scope or a variable.
|
66
|
+
#
|
67
|
+
# Attributes of {#type} `:misc` do not need a matching `:attrend`.
|
68
|
+
class Attribute
|
69
|
+
# @return [Symbol] Either `:misc`, `:array`, `:enum` or `:pack`
|
70
|
+
attr_reader :type
|
71
|
+
|
72
|
+
# When `@type` is
|
73
|
+
# - __:misc__: `:comment`, `:envvar`, `:supvar`, `:pathname`, `:sourcestem`, `:sourceistem`, `:valuelist`, `:enumtable`, `:unknown`
|
74
|
+
# - __:array__: `:none`, `:unpacked`, `:packed` or `:sparse`
|
75
|
+
# - __:enum__: `:integer`, `:bit`, `:logic`, `:int`, `:shortint`, `:longint`, `:byte`, `:unsigned_integer`, `:unsigned_bit`, `:unsigned_logic`, `:unsigned_int`, `:unsigned_shortint`, `:unsigned_longint`, `:unsigned_byte`, `:reg` or `:time`
|
76
|
+
# - __:pack__: `:none`, `:unpacked`, `:packed` or `:tagged_packed`
|
77
|
+
# @return [Symbol]
|
78
|
+
attr_reader :subtype
|
79
|
+
|
80
|
+
attr_reader :name # @return [String]
|
81
|
+
attr_reader :arg # @return [Integer]
|
82
|
+
attr_reader :arg_from_name # @return [Integer]
|
83
|
+
|
84
|
+
# @!visibility private
|
85
|
+
def initialize(type, subtype, name, arg, arg_from_name)
|
86
|
+
@type = type
|
87
|
+
@subtype = subtype
|
88
|
+
@name = name
|
89
|
+
@arg = arg
|
90
|
+
@arg_from_name = arg_from_name
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [String]
|
94
|
+
def to_s
|
95
|
+
"<#{self.class.name} #{@name}, type: #{@type}, subtype: #{@subtype}, arg: #{@arg}, arg_from_name: #{@arg_from_name}>"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# A Variable corresponds to a signal in the hierarchy of the design.
|
100
|
+
# It points to a {Trace}.
|
101
|
+
# Different variables can point to the same {Trace} : for example the clock signal from different modules is actualy the same
|
102
|
+
# physical signal.
|
103
|
+
class Variable
|
104
|
+
attr_reader :type # @return [Symbol] Either `:vcd_event`, `:vcd_integer`, `:vcd_parameter`, `:vcd_real`, `:vcd_real_parameter`, `:vcd_reg`, `:vcd_supply0`, `:vcd_supply1`, `:vcd_time`, `:vcd_tri`, `:vcd_triand`, `:vcd_trior`, `:vcd_trireg`, `:vcd_tri0`, `:vcd_tri1`, `:vcd_wand`, `:vcd_wire`, `:vcd_wor`, `:vcd_port`, `:vcd_sparray`, `:vcd_realtime`, `:gen_string`, `:sv_bit`, `:sv_logic`, `:sv_int`, `:sv_shortint`, `:sv_longint`, `:sv_byte`, `:sv_enum` or `:sv_shortreal`
|
105
|
+
attr_reader :direction # @return [Symbol] Either `:implicit`, `:input`, `:output`, `:inout`, `:buffer` or `:linkage`
|
106
|
+
attr_reader :name # @return [String]
|
107
|
+
attr_reader :length # @return [Integer]
|
108
|
+
attr_reader :handle # @return [Integer]
|
109
|
+
attr_reader :is_alias # @return [Boolean]
|
110
|
+
attr_accessor :trace # @return [Trace]
|
111
|
+
attr_accessor :scope # @return [Scope]
|
112
|
+
attr_accessor :attributes # @return [Array<Attribute>]
|
113
|
+
|
114
|
+
# @!visibility private
|
115
|
+
def initialize(type, direction, name, length, handle, is_alias)
|
116
|
+
@type = type
|
117
|
+
@direction = direction
|
118
|
+
@name = name
|
119
|
+
@length = length
|
120
|
+
@handle = handle
|
121
|
+
@is_alias = is_alias
|
122
|
+
@attributes = []
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [String]
|
126
|
+
def path
|
127
|
+
return @path if @path
|
128
|
+
p = "/#{name}"
|
129
|
+
p = @scope.path + p if @scope
|
130
|
+
@path = p
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [String]
|
134
|
+
def to_s
|
135
|
+
"<#{self.class.name} #{path} type: #{@type}, dir: #{@direction}, len: #{@length}, handle: #{@handle}, is_alias: #{!!@is_alias}>"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# A Scope corresponds to a design element
|
141
|
+
class Scope
|
142
|
+
attr_reader :type # @return [Symbol] Either `:vcd_module`, `:vcd_task`, `:vcd_function`, `:vcd_begin`, `:vcd_fork`, `:vcd_generate`, `:vcd_struct`, `:vcd_union`, `:vcd_class`, `:vcd_interface`, `:vcd_package`, `:vcd_program`, `:vhdl_architecture`, `:vhdl_procedure`, `:vhdl_function`, `:vhdl_record`, `:vhdl_process`, `:vhdl_block`, `:vhdl_for_generate`, `:vhdl_if_generate`, `:vhdl_generate` or `:vhdl_package`
|
143
|
+
attr_reader :name # @return [String]
|
144
|
+
attr_reader :component # @return [String,nil]
|
145
|
+
attr_reader :variables # @return [Array<Variable>]
|
146
|
+
attr_accessor :attributes # @return [Array<Attribute>]
|
147
|
+
attr_accessor :parent # @return [Scope, nil]
|
148
|
+
attr_reader :children # @return [Array<Scope>]
|
149
|
+
|
150
|
+
# @!visibility private
|
151
|
+
def initialize(type, name, component)
|
152
|
+
@type = type
|
153
|
+
@name = name
|
154
|
+
@component = component
|
155
|
+
@variables = []
|
156
|
+
@attributes = []
|
157
|
+
@parent = nil
|
158
|
+
@children = []
|
159
|
+
end
|
160
|
+
|
161
|
+
# @return [String]
|
162
|
+
def path
|
163
|
+
return @path if @path
|
164
|
+
s = self
|
165
|
+
p = ''
|
166
|
+
until s.nil?
|
167
|
+
p = "/#{s.name}#{p}"
|
168
|
+
s = s.parent
|
169
|
+
end
|
170
|
+
@path = p
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [String]
|
174
|
+
def to_s
|
175
|
+
"<#{self.class.name} #{path}, type: #{@type}, component: \"#{@component}\">"
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [String]
|
179
|
+
# @overload tree
|
180
|
+
def tree(prefix = '', last: true, first: true)
|
181
|
+
str = prefix + (first ? '' : (last ? '└─ ' : '├─ '))
|
182
|
+
str += "\e[34;1m#{@name}\e[0m (#{@type})\n"
|
183
|
+
prefix += (first ? '' : (last ? ' ' : '│ '))
|
184
|
+
mxvarnamelen = ([0] + @variables.collect{|v| v.name.length}).max
|
185
|
+
mxlenlen = ([0] + @variables.collect { |v| v.length == 0 ? 0 : v.length.to_s.length }).max
|
186
|
+
@variables.each_with_index do |v, i|
|
187
|
+
l = (@children.empty? and (i+1 == @variables.length))
|
188
|
+
str += prefix + (l ? '└─ ' : '├─ ') + v.name + ' '*(mxvarnamelen+1-v.name.length)
|
189
|
+
str += case v.direction
|
190
|
+
when :input then ' I '
|
191
|
+
when :output then ' O '
|
192
|
+
when :inout then 'IO '
|
193
|
+
else ' '
|
194
|
+
end
|
195
|
+
str += (v.length > 0 ? v.length.to_s : '').rjust(mxlenlen)
|
196
|
+
str += " #{v.type}\n"
|
197
|
+
end
|
198
|
+
@children.each_with_index do |c, i|
|
199
|
+
str += c.tree(prefix, (i+1 == @children.length), false)
|
200
|
+
end
|
201
|
+
str
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
class Reader
|
208
|
+
attr_reader :filename # @return [String]
|
209
|
+
attr_reader :traces # @return [Array<Trace>]
|
210
|
+
attr_reader :variables # @return [Array<Variable>]
|
211
|
+
attr_reader :root # @return [Scope]
|
212
|
+
|
213
|
+
def initialize(filename)
|
214
|
+
@filename = File.expand_path(filename)
|
215
|
+
open(@filename)
|
216
|
+
@traces = max_handle.times.collect { |i| Trace.new(i+1) }
|
217
|
+
@variables = []
|
218
|
+
travel_hierarchy
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
# Find a {Trace} by its name or path
|
223
|
+
# @param name [String] name or path to look for
|
224
|
+
# @return [Trace,nil]
|
225
|
+
def trace(name)
|
226
|
+
@traces.each do |t|
|
227
|
+
return t if t.name == name or t.path == name
|
228
|
+
end
|
229
|
+
nil
|
230
|
+
end
|
231
|
+
|
232
|
+
# Find a {Variable} by its name or path
|
233
|
+
# @param name [String] name or path to look for
|
234
|
+
# @return [Variable,nil]
|
235
|
+
def variable(name)
|
236
|
+
@variables.each do |v|
|
237
|
+
return v if v.name == name or v.path == name
|
238
|
+
end
|
239
|
+
nil
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns the scope or variable corresponding to the specified path
|
243
|
+
# @param path [String]
|
244
|
+
# @return [Scope,Variable,nil]
|
245
|
+
def [](path)
|
246
|
+
raise ArgumentError, "expecting a String, not a #{path.class}" unless path.is_a?(String)
|
247
|
+
a = path.split('/').reject(&:empty?)
|
248
|
+
return nil if a.empty?
|
249
|
+
return nil if a[0] != @root.name
|
250
|
+
scope = @root
|
251
|
+
var = nil
|
252
|
+
a[1..].each do |w|
|
253
|
+
var = scope.variables.select { |v| v.name == w }.first
|
254
|
+
return var if var
|
255
|
+
s = scope.children.select { |v| v.name == w }.first
|
256
|
+
return nil if s.nil?
|
257
|
+
scope = s
|
258
|
+
end
|
259
|
+
scope
|
260
|
+
end
|
261
|
+
|
262
|
+
# Pretty print the integer time given as parametter according to the time scale
|
263
|
+
# @param timei [Integer]
|
264
|
+
# @return [String]
|
265
|
+
def pretty_time(timei)
|
266
|
+
return '0 s' if timei == 0
|
267
|
+
i = time_scale_exponent
|
268
|
+
e = (i/3)*3
|
269
|
+
f = 10**(i - e)
|
270
|
+
timei *= f
|
271
|
+
loop do
|
272
|
+
break if e >= 0
|
273
|
+
m = timei % 1000
|
274
|
+
break if m != 0
|
275
|
+
timei /= 1000
|
276
|
+
e += 3
|
277
|
+
end
|
278
|
+
s = timei.to_s
|
279
|
+
l = s.length
|
280
|
+
i = ((l-1)/3)*3
|
281
|
+
return "#{timei} #{['', 'm', 'µ', 'n', 'p', 'f'][-e/3]}s" if i <= 0 or e+i > 0
|
282
|
+
e += i
|
283
|
+
ent = s[0...l-i]
|
284
|
+
frac = s[l-i..].sub(/0+$/, '')
|
285
|
+
"#{ent}.#{frac} #{['', 'm', 'µ', 'n', 'p', 'f'][-e/3]}s"
|
286
|
+
end
|
287
|
+
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
# Build up the design hierarchy
|
292
|
+
def travel_hierarchy
|
293
|
+
scopes = []
|
294
|
+
attributes = []
|
295
|
+
iterate_hier_rewind
|
296
|
+
loop do
|
297
|
+
h = iterate_hier
|
298
|
+
break if h.nil?
|
299
|
+
case h
|
300
|
+
when Scope
|
301
|
+
unless attributes.empty? then
|
302
|
+
h.attributes = attributes
|
303
|
+
attributes = []
|
304
|
+
end
|
305
|
+
if scopes.empty? then
|
306
|
+
raise 'what?' unless @root.nil?
|
307
|
+
@root = h
|
308
|
+
else
|
309
|
+
p = scopes.last
|
310
|
+
h.parent = p
|
311
|
+
p.children.push h
|
312
|
+
end
|
313
|
+
scopes.push h
|
314
|
+
when Variable
|
315
|
+
trace = @traces[h.handle-1]
|
316
|
+
h.trace = trace
|
317
|
+
trace.variables.push h
|
318
|
+
unless attributes.empty? then
|
319
|
+
h.attributes = attributes
|
320
|
+
attributes = []
|
321
|
+
end
|
322
|
+
@variables.push h
|
323
|
+
if scopes.empty? then
|
324
|
+
raise 'what?' unless @root.nil?
|
325
|
+
p = Scope.new(:vcd_module, 'root', nil)
|
326
|
+
scopes.push p
|
327
|
+
@root = p
|
328
|
+
end
|
329
|
+
scopes.last.variables.push h
|
330
|
+
h.scope = scopes.last
|
331
|
+
trace.is_float = true if h.type == :vcd_real or h.type == :vcd_real_parameter or h.type == :vcd_realtime or h.type == :sv_shortreal
|
332
|
+
when Attribute
|
333
|
+
attributes << h
|
334
|
+
when :upscope
|
335
|
+
scopes.pop
|
336
|
+
when :attrend
|
337
|
+
raise 'What attrend is used for?'
|
338
|
+
end
|
339
|
+
end
|
340
|
+
iterate_hier_rewind
|
341
|
+
self
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
data/lib/libfst/tfp.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Copyright (C) 2024 Théotime Bollengier <theotime.bollengier@ensta-bretagne.fr>
|
2
|
+
#
|
3
|
+
# This file is part of libfst.rb <https://gitlab.ensta-bretagne.fr/bollenth/libfst.rb>
|
4
|
+
#
|
5
|
+
# libfst.rb is free software: you can redistribute it and/or modify it
|
6
|
+
# under the terms of the GNU General Public License as published
|
7
|
+
# by the Free Software Foundation, either version 3 of the License,
|
8
|
+
# or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# libfst.rb is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
13
|
+
# See the GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with libfst.rb. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require_relative './vcd'
|
19
|
+
|
20
|
+
# This module handles GTKWave specific stuf.
|
21
|
+
module GTKWave
|
22
|
+
# This is a base class which ease the creation of GTKWave Transaction Filter Process.
|
23
|
+
#
|
24
|
+
# Such a process is called by GTKWave which feeds it some VCD on the standard input,
|
25
|
+
# the process decodes what it can from the input then writes some kind of VCD on the
|
26
|
+
# standard output, which is interpreted and displayed by GTKWave.
|
27
|
+
class TransactionFilter
|
28
|
+
attr_reader :ivcd # @return [VCD::Reader]
|
29
|
+
attr_reader :name # @return [String]
|
30
|
+
attr_reader :data_start_tocken # @return [Integer]
|
31
|
+
attr_reader :min_time # @return [Integer]
|
32
|
+
attr_reader :max_time # @return [Integer]
|
33
|
+
attr_reader :max_seqn # @return [Integer]
|
34
|
+
attr_reader :args # @return [String]
|
35
|
+
attr_reader :signames # @return [Array<String>] Signal names ordered according to "seqn"
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@ivcd = VCD::Reader.new($stdin)
|
39
|
+
exit 0 if $stdin.eof?
|
40
|
+
|
41
|
+
@signames = []
|
42
|
+
|
43
|
+
@ivcd.comments.each do |cmnt|
|
44
|
+
case cmnt
|
45
|
+
when /^name (.+)$/
|
46
|
+
@name = $1
|
47
|
+
when /^data_start 0x(\h+)$/
|
48
|
+
@data_start_tocken = $1.to_i(16)
|
49
|
+
when /^min_time (\d+)$/
|
50
|
+
@min_time = $1.to_i
|
51
|
+
when /^max_time (\d+)$/
|
52
|
+
@max_time = $1.to_i
|
53
|
+
when /^max_seqn (\d+)$/
|
54
|
+
@max_seqn = $1.to_i
|
55
|
+
when /^seqn (\d+) (.+)$/
|
56
|
+
@signames[$1.to_i - 1] = $2
|
57
|
+
when /^args "(.*?)"$/
|
58
|
+
@args = $1.split(/\s*;\s*/).reject{|v| v.empty?}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
raise "Cannot find name comment" if @name.nil?
|
63
|
+
raise "Cannot find data_start comment" if @data_start_tocken.nil?
|
64
|
+
raise "Cannot find min_time comment" if @min_time.nil?
|
65
|
+
raise "Cannot find max_time comment" if @max_time.nil?
|
66
|
+
raise "Cannot find max_seqn comment" if @max_seqn.nil?
|
67
|
+
raise "Cannot find args comment" if @args.nil?
|
68
|
+
|
69
|
+
@ivcd.on_comment do |cmnt|
|
70
|
+
m = cmnt.match(/^data_end 0x(\h+)$/)
|
71
|
+
next if m.nil?
|
72
|
+
@ivcd.stop_parsing if m[1].to_i(16) == @data_start_tocken
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def read
|
79
|
+
@ivcd.read
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def self.run
|
84
|
+
loop do
|
85
|
+
read_all_input = false
|
86
|
+
begin
|
87
|
+
decoder = self.new
|
88
|
+
decoder.read
|
89
|
+
read_all_input = true
|
90
|
+
decoder.write
|
91
|
+
rescue => e
|
92
|
+
$stderr.puts e.full_message(highlight: true, order: :top)
|
93
|
+
# $stderr.puts e.message
|
94
|
+
unless read_all_input then
|
95
|
+
#$stderr.puts "Flushing STDIN..."
|
96
|
+
$stdin.each_line do |line|
|
97
|
+
#$stderr.puts line
|
98
|
+
break if line =~ /^\$comment\s+data_end\s+0x\h+\s+\$end\n$/
|
99
|
+
end
|
100
|
+
#$stderr.puts "DONE"
|
101
|
+
end
|
102
|
+
$stdout.puts '$name DECODE_ERROR'
|
103
|
+
$stdout.puts "#0 ?red?#{e.message}"
|
104
|
+
$stdout.puts '$finish'
|
105
|
+
$stdout.flush
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|