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.
@@ -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
+