tla2dot 0.0.3

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,415 @@
1
+ # -*- mode: ruby -*-
2
+ #
3
+ # Parse state dump from TCL tlaplus
4
+
5
+ class Tla2DotParser
6
+
7
+
8
+
9
+ options no_result_var
10
+
11
+ rule
12
+
13
+ target : entries { @graph }
14
+
15
+ entries : entry
16
+ | entries entry
17
+
18
+ entry : state { @graph.add_node( val[0] ) }
19
+ | trans
20
+
21
+
22
+ trans : TRANS '-->' INTEGER { @graph.add_edge( val[0], val[2] ) }
23
+
24
+
25
+ # state : STATE variables { Node.new( val[0], val[1] ) }
26
+ # | STATE { Node.new( val[0], {} ) }
27
+
28
+ state : state_def variables { val[0].variables=val[1]; val[0] }
29
+ | state_def
30
+
31
+ state_def : state_id
32
+ | state_id actiondef
33
+
34
+ state_id : STATE { Node.new( val[0], {} ) }
35
+
36
+
37
+ actiondef : '<' actionspecs '>'
38
+
39
+ actionspecs : actionspec
40
+ | actionspecs actionspec
41
+
42
+ actionspec : IDENT
43
+ | ','
44
+ | INTEGER
45
+
46
+ variables : variable
47
+ | variables variable { k=val[1].keys.first; val[0][k] = val[1][k]; val[0] }
48
+
49
+ variable : AND name '=' value { { val[1] => val[3] } }
50
+
51
+ name : IDENT
52
+
53
+ value : INTEGER { val[0].to_i }
54
+ | STRING
55
+ | IDENT
56
+ | record
57
+ | set
58
+ | seq
59
+
60
+ record : '[' ']' { {} }
61
+ | '[' rlist ']' { val[1] }
62
+
63
+ rlist : ritem { val[0] }
64
+ | rlist ',' ritem { k=val[2].keys.first; val[0][k] = val[2][k]; val[0] }
65
+
66
+ ritem : name '|->' value { { val[0] => val[2] } }
67
+
68
+
69
+ seq : '<' '<' '>' '>' { [] }
70
+ | '<' '<' seqlist '>' '>' { val[2] }
71
+
72
+ seqlist : seqitem
73
+ | seqlist ',' seqitem { val[0].push( val[2][0] ); val[0] }
74
+
75
+ seqitem : value { [ val[0] ] }
76
+
77
+
78
+ set : '{' '}' { [] }
79
+ | '{' slist '}' { val[1] }
80
+
81
+ slist : sitem
82
+ | slist ',' sitem { val[0].push( val[2][0] ); val[0] }
83
+
84
+ sitem : value { [ val[0] ] }
85
+
86
+ end
87
+
88
+ ---- inner
89
+
90
+ class Node
91
+
92
+ include TLA2DOT::Utils::MyLogger # mix logger
93
+
94
+ @@logger = nil; # common logger for all nodes
95
+ @@options = {}; # common logger for all nodes
96
+
97
+
98
+ # ------------------------------------------------------------------
99
+ # Attributes
100
+
101
+ # instance
102
+ attr_accessor :name #
103
+ attr_accessor :variables # hash of name=>
104
+ attr_accessor :edges # list of nodes reachable
105
+
106
+ # ------------------------------------------------------------------
107
+ # constructore
108
+ def initialize( name, variables )
109
+ @name = name
110
+ @variables = variables
111
+ @edges = []
112
+ @filter_render_variables = []
113
+ end
114
+
115
+ def myLogger
116
+ return @@logger if @@logger
117
+ @@logger = getLogger( "Node", getOptions )
118
+ @@logger.info( "#{__method__} created" )
119
+ @@logger
120
+ end
121
+
122
+ def setOptions( options )
123
+ @@options = options
124
+ end
125
+
126
+ def getOptions
127
+ @@options
128
+ end
129
+
130
+ def name
131
+ @name
132
+ end
133
+
134
+ def variables=( variables )
135
+ @variables = variables
136
+ end
137
+
138
+ def variables
139
+ @variables
140
+ end
141
+
142
+ def ==( node )
143
+ node.respond_to?( :name) && self.name == node.name
144
+ end
145
+
146
+ def add_edge( node )
147
+ @edges << node unless @edges.include?( node )
148
+ end
149
+
150
+ def successor_cnt
151
+ @edges.size
152
+ end
153
+
154
+ # array of successors for the node
155
+ def successors
156
+ @edges
157
+ end
158
+
159
+ # array tranitions (from_state->to_state) from this node to another
160
+ def transitions
161
+ myLogger.debug( "#{__method__} starting" )
162
+ successors.select{ |n| n.name != name }.map{ |n| { :to_state => n.name, :from_state => self.name } }
163
+ end
164
+
165
+ # array of variable names to render
166
+ def filter_render_variables
167
+ myLogger.debug( "#{__method__} filter_render_variables=#{@filter_render_variables} #{@filter_render_variables.class}" )
168
+ @filter_render_variables
169
+ end
170
+
171
+ # called when added to graph
172
+ def filter_render_variables=( variables )
173
+
174
+ @filter_render_variables = variables
175
+ end
176
+
177
+ # return true is state should show variable
178
+ def include_variable( variable )
179
+
180
+ return filter_render_variables.include?( variable ) if
181
+ filter_render_variables.kind_of?( Array )
182
+
183
+ # assume boolean
184
+ return filter_render_variables
185
+
186
+ end
187
+
188
+
189
+ # node variable to render as key/state hash (including 'id')
190
+ def content
191
+ myLogger.debug( "#{__method__} starting" )
192
+ vars = variables;
193
+ vars['ID'] = name
194
+ vars.select{ |k,v| include_variable( k ) }.
195
+ map { |k,v|
196
+ {:key =>k, :val => v, :state => render_value(v) }
197
+ }
198
+
199
+ end
200
+
201
+ # escape for mustache rendering
202
+ def render_value( v )
203
+ return v.to_s.
204
+ gsub( /\{/, "\\{" ).
205
+ gsub( /\}/, "\\}" ).
206
+ gsub( /=>/, "=" ).
207
+ # gsub( /\{/, "" ).
208
+ # gsub( /\}/, "" ).
209
+ # gsub( /\[/, "" ).
210
+ # gsub( /\]/, "" ).
211
+ # gsub( /\[/, "\\[" ).
212
+ # gsub( /\]/, "\\]" ).
213
+ gsub( /"/, '\\"' )
214
+ # case
215
+ # when v.kind_of?( Hash )
216
+ # return v.to_s.gsub( /\{/, "\\{" ).gsub( /\}/, "\\}" )
217
+ # else
218
+ # return v
219
+ # end
220
+
221
+ end
222
+
223
+
224
+ end
225
+
226
+
227
+ class Graph
228
+
229
+ include TLA2DOT::Utils::MyLogger # mix logger
230
+
231
+
232
+ attr_reader :nodes #
233
+
234
+ def initialize( filter_render_variables, options )
235
+ @nodes = {}
236
+ # filter_render_variables passed to node
237
+ @filter_render_variables = filter_render_variables
238
+ setOptions( options )
239
+ end
240
+
241
+ def myLogger
242
+ return @logger if @logger
243
+ @logger = getLogger( "Graph", getOptions )
244
+ @logger.info( "#{__method__} created" )
245
+ @logger
246
+ end
247
+
248
+
249
+ def setOptions( options )
250
+ @options = options
251
+ end
252
+
253
+ def getOptions
254
+ @options
255
+ end
256
+
257
+ def add_node(node)
258
+ node.filter_render_variables=( @filter_render_variables )
259
+ node.setOptions( getOptions )
260
+ myLogger.debug( "#{__method__} added node #{node}, @filter_render_variables=#{@filter_render_variables} #{@filter_render_variables.class}" )
261
+ @nodes[node.name] = node
262
+ self
263
+ end
264
+
265
+
266
+ def nodes
267
+ @nodes
268
+ end
269
+
270
+ def states
271
+ @nodes.values
272
+ end
273
+
274
+
275
+ def node_cnt
276
+ @nodes.size
277
+ end
278
+
279
+
280
+ def add_edge(predecessor_name, successor_name)
281
+
282
+ predecessor_node = @nodes[predecessor_name]
283
+ raise "Unknown precessor node #{predecessor_name}" unless predecessor_node
284
+ successor_node = @nodes[successor_name]
285
+ raise "Unknown precessor node #{successor_name}" unless successor_node
286
+ @nodes[predecessor_name].add_edge(@nodes[successor_name])
287
+ end
288
+
289
+ def [](name)
290
+ @nodes[name]
291
+ end
292
+ end
293
+
294
+ # ------------------------------------------------------------------
295
+
296
+ include TLA2DOT::Utils::MyLogger # mix logger
297
+ PROGNAME = "parser" # progname for logger
298
+
299
+ def initialize( options = {} )
300
+ @logger = getLogger( PROGNAME, options )
301
+ @logger.debug( "#{__method__} initialized" )
302
+ setOptions( options )
303
+ end
304
+
305
+ def setOptions( options )
306
+ @options = options
307
+ end
308
+
309
+ def getOptions
310
+ @options
311
+ end
312
+
313
+
314
+
315
+ # entry point
316
+ def parse(str, filter_render_variables = [] )
317
+ @logger.info( "#{__method__} parsing started" )
318
+ @graph = Graph.new( filter_render_variables, getOptions )
319
+ @line = 0
320
+ return @graph if str.nil? || str.empty?
321
+ @str = str.kind_of?( Array ) ? str : [ str ]
322
+ begin
323
+ ret = yyparse self, :scan
324
+ @logger.info( "#{__method__} parsing done" )
325
+ return ret
326
+ rescue Exception => e
327
+ puts "Error on line #{@line} near #{@current}"
328
+ @logger.error( "#{__method__} error #{e}" )
329
+ # puts e
330
+ return nil
331
+ end
332
+ end
333
+
334
+ private
335
+
336
+ # return next line for scanner
337
+ def next_str
338
+ @current = @str.any? ? @str.shift : nil
339
+ @line += 1
340
+ @current
341
+ end
342
+
343
+
344
+ # racc token scanner
345
+ def scan
346
+ while true
347
+ str = next_str
348
+ break if str.nil?
349
+ until str.empty?
350
+ case str
351
+ when /\A\s+/
352
+ str = $'
353
+ when /\AState (\d+):/
354
+ yield [ :STATE, $1 ]
355
+ str = $'
356
+ when /\AState (\d+)\/(-?\d+):/
357
+ # using fingerprint name
358
+ yield [ :STATE, $2 ]
359
+ str = $'
360
+ when /\AState (\d+):/
361
+ yield [ :STATE, $1 ]
362
+ str = $'
363
+ when /\ATransition ([0-9\-\+]+)/
364
+ yield [ :TRANS, $1 ]
365
+ str = $'
366
+ when /\A"([^"]*)"*/
367
+ yield [ :STRING, $1 ]
368
+ str = $'
369
+ when /\A[a-zA-Z]\w*/
370
+ yield [ :IDENT, $& ]
371
+ str = $'
372
+ when /\A-?\d+/
373
+ yield [ :INTEGER, $& ]
374
+ str = $'
375
+ when /\A\/\\/
376
+ yield [ :AND, $& ]
377
+ str = $'
378
+ when /\A\|->/
379
+ yield [ $&, $& ]
380
+ str = $'
381
+ # when /\A\n/
382
+ # puts "Nl"
383
+ # yield [ :NL, $& ]
384
+ # str = $'
385
+ when /\A\-->/
386
+ yield [ $&, $& ]
387
+ str = $'
388
+ when /\A=/
389
+ yield [ $&, $& ]
390
+ str = $'
391
+ else
392
+ c = str[0,1]
393
+ yield [ c, c ]
394
+ str = str[1..-1]
395
+ end
396
+ end # until
397
+ end # while
398
+ yield [ false, '$'] # is optional from Racc 1.3.7
399
+ end
400
+
401
+ ---- footer
402
+
403
+ if $0 == __FILE__
404
+ src = <<EOS
405
+ {
406
+ name => MyName,
407
+ id => MyIdent
408
+ }
409
+ EOS
410
+ puts 'Parsing (String):'
411
+ print src
412
+ puts
413
+ puts 'Result (Ruby Object):'
414
+ p HashParser.new.parse(src)
415
+ end
@@ -0,0 +1,184 @@
1
+ require 'mustache' # extendending implementation of
2
+
3
+ module TLA2DOT
4
+
5
+ class Template < Mustache
6
+
7
+ include TLA2DOT::Utils::MyLogger # mix logger
8
+ PROGNAME = "template" # progname for logger
9
+
10
+ # ------------------------------------------------------------------
11
+ # Attributes
12
+
13
+ # instance
14
+ attr_writer :partials # f: partial-name --> template string
15
+ attr_writer :templates # f: template-name --> template string
16
+
17
+ # ------------------------------------------------------------------
18
+ # Constructor
19
+
20
+ def initialize( options={} )
21
+ @logger = getLogger( PROGNAME, options )
22
+ @logger.info( "#{__method__} created" )
23
+ @logger.debug( "#{__method__}, options='#{options}" )
24
+
25
+ @template_extension = "mustache"# type part in filename
26
+
27
+ # for mustache templates
28
+ if options[:templates] then
29
+ @template_paths = options[:templates]
30
+ else
31
+ @template_paths = TLA2DOT::Cli::TEMPLATES
32
+ end
33
+ # init partial cache
34
+ @partials = {}
35
+
36
+ end
37
+
38
+ # ------------------------------------------------------------------
39
+ # Services
40
+
41
+ def to_str( template_name, data )
42
+ @logger.info( "#{__method__}: template_name=#{template_name}, data=#{data}, nodes.size=#{data.nodes.size}" )
43
+ # @logger.debug( "#{__method__}: nodes=#{data.nodes}" )
44
+ @data = data
45
+ template = get_template( template_name )
46
+ render( template, { :tsti=>"moikka", :data=>data, "tst" => "hello" } )
47
+
48
+ end
49
+
50
+
51
+ # def data
52
+ # @data
53
+ # end
54
+
55
+
56
+
57
+ # ------------------------------------------------------------------
58
+ # Integrate with mustache
59
+
60
+ # method used by mustache framework - delegate to 'get_partial'
61
+ def partial(name)
62
+ @logger.debug( "#{__method__} name=#{name}" )
63
+ get_partial( name )
64
+ end
65
+
66
+ # cache @partials - for easier extension
67
+ def get_partial( name )
68
+ @logger.debug( "#{__method__} name=#{name}" )
69
+ return @partials[name] if @partials[name]
70
+
71
+ partial_file = get_template_filepath( name )
72
+ @logger.info( "#{__method__} read partial_file=#{partial_file}" )
73
+ @partials[name] = File.read( partial_file )
74
+ @partials[name]
75
+ end
76
+
77
+ # hide @templates - for easier extension
78
+ def get_template( name )
79
+
80
+ template_file = get_template_filepath( name )
81
+ @logger.info( "#{__method__} read template_file=#{template_file}" )
82
+ File.read( template_file )
83
+
84
+ end # def get_template( name )
85
+
86
+
87
+ # return path to an existing template file name
88
+ private
89
+
90
+ def get_template_filepath( name )
91
+
92
+ @template_paths.each do |directory_or_gemname|
93
+
94
+ template_path = get_template_filepath_resolve( directory_or_gemname, name )
95
+
96
+ return template_path if File.exists?( template_path )
97
+
98
+ end # each
99
+
100
+
101
+ # could not find
102
+
103
+ raise <<-eos
104
+
105
+ No such template '#{name}' found in directories: #{@template_paths.join(", ")}
106
+
107
+ Use opition -t list directories or Gems, where file '#{name}.#{@template_extension}' can be located.
108
+
109
+ eos
110
+
111
+
112
+ end
113
+
114
+ # return path to plain 'directory' or to gem directory
115
+ def get_template_filepath_resolve( directory_or_gemname, template_file )
116
+ @logger.info( "#{__method__} directory_or_gemname=#{directory_or_gemname}" )
117
+ if directory_or_gemname[-1] == '/' then
118
+ return get_template_filepath_in_directory( directory_or_gemname[0..-2], template_file )
119
+ else
120
+ directory = gemname_to_directory( directory_or_gemname )
121
+ return get_template_filepath_in_directory( directory, template_file )
122
+ end
123
+ end
124
+
125
+ # return directory to 'gemspec'
126
+ def gemname_to_directory( gemname_and_spec )
127
+
128
+ # The version requirements are optional.
129
+ # You can also specify multiple version requirements, just append more at the end
130
+ gem_spec = gemname_and_spec.split(',')
131
+ gem_name, *gem_ver_reqs = gem_spec[0], gem_spec[1]
132
+ @logger.debug( "#{__method__}, gem_name=#{gem_name}, *gem_ver_reqs=#{gem_ver_reqs}" )
133
+ gdep = Gem::Dependency.new(gem_name, *gem_ver_reqs)
134
+ # find latest that satisifies
135
+ found_gspec = gdep.matching_specs.sort_by(&:version).last
136
+ @logger.debug( "#{__method__}, found_gspec=#{found_gspec}" )
137
+ # instead of using Gem::Dependency, you can also do:
138
+ # Gem::Specification.find_all_by_name(gem_name, *gem_ver_reqs)
139
+
140
+ if found_gspec
141
+ @logger.debug( "#{__method__}, Requirement '#{gdep}' already satisfied by #{found_gspec.name}-#{found_gspec.version}" )
142
+ template_path = "#{found_gspec.gem_dir}/mustache"
143
+ @logger.debug( "#{__method__}, template_path=#{template_path}" )
144
+ else
145
+ #puts "Requirement '#{gdep}' not satisfied; installing..."
146
+ # ver_args = gdep.requirements_list.map{|s| ['-v', s] }.flatten
147
+ # # multi-arg is safer, to avoid injection attacks
148
+ # system('gem', 'install', gem_name, *ver_args)
149
+ raise "Could not find gem '#{gdep}' - try 'gem install #{gdep}'"
150
+ end
151
+
152
+ return template_path
153
+
154
+ end
155
+
156
+
157
+ # return path to 'template_file' in an existing 'directory'
158
+ def get_template_filepath_in_directory( directory, template_file )
159
+ @logger.debug( "#{__method__} directory=#{directory}, template_file=#{template_file}" )
160
+
161
+ if ! File.exists?( directory ) then
162
+ raise <<-eos
163
+
164
+ No such directory '#{directory}'.
165
+
166
+ Option -t should list
167
+ - existing paths OR
168
+ - Gems which includes 'mustache' directory
169
+
170
+ eos
171
+
172
+ end
173
+
174
+ template_path = "#{directory}/#{template_file}.#{@template_extension}"
175
+ @logger.info( "#{__method__} read template_path=#{template_path}" )
176
+
177
+ return template_path
178
+
179
+ end
180
+
181
+
182
+ end # class
183
+
184
+ end # module
data/lib/tla2dot.rb ADDED
@@ -0,0 +1,10 @@
1
+ # tla2dot.rb
2
+
3
+ require_relative "utils/logger.rb"
4
+
5
+ require_relative "tla2dot/parser.tab.rb"
6
+ require_relative "tla2dot/template.rb"
7
+ require_relative "cli/cli.rb"
8
+
9
+
10
+
@@ -0,0 +1,70 @@
1
+ require 'logger'
2
+
3
+ # see http://hawkins.io/2013/08/using-the-ruby-logger/
4
+
5
+ module TLA2DOT
6
+
7
+ module Utils
8
+
9
+ module MyLogger
10
+
11
+ # no logging done
12
+
13
+ class NullLoger < Logger
14
+ def initialize(*args)
15
+ end
16
+
17
+ def add(*args, &block)
18
+ end
19
+ end
20
+
21
+ LOGFILE="tla2dot.log"
22
+
23
+ def getLogger( progname, options={} )
24
+
25
+ level = get_level( options )
26
+
27
+ if level.nil?
28
+
29
+ return NullLoger.new
30
+
31
+ else
32
+
33
+ logger = Logger.new( LOGFILE )
34
+ logger.level=level
35
+ logger.progname = progname
36
+ return logger
37
+
38
+ end
39
+
40
+ end # getLogger
41
+
42
+ # ------------------------------------------------------------------
43
+ private
44
+
45
+ def get_level( options )
46
+
47
+ # puts "#{__method__}: options=#{options}"
48
+
49
+ level_name = options && options[:log] ? options[:log] : ENV['LOG_LEVEL']
50
+
51
+ level = case level_name
52
+ when 'warn', 'WARN'
53
+ Logger::WARN
54
+ when 'info', 'INFO'
55
+ Logger::INFO
56
+ when 'debug', 'DEBUG'
57
+ Logger::DEBUG
58
+ when 'error', 'ERROR'
59
+ Logger::ERROR
60
+ else
61
+ nil
62
+ end
63
+
64
+ return level
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,19 @@
1
+ fontname = "Bitstream Vera Sans";
2
+ fontsize = 8;
3
+ shape = "record";
4
+ labeljust="l";
5
+ rankdir=LR;
6
+
7
+
8
+ node [ fontname = "Courier"
9
+ fontsize = 8
10
+ shape = "record"
11
+
12
+ ];
13
+
14
+ edge [
15
+ fontname = "Bitstream Vera Sans"
16
+ fontsize = 8
17
+ arrowhead = "none"
18
+ ];
19
+
@@ -0,0 +1,20 @@
1
+ digraph G {
2
+
3
+ /* font size layout etc. */
4
+ {{> defaults }}
5
+
6
+ /* nodes */
7
+ {{# data }}
8
+ {{# states}}
9
+ {{name}} [{{> stateOutput }}];
10
+ {{/ states}}
11
+ {{/ data }}
12
+
13
+ /* arcs */
14
+ {{# data }}
15
+ {{# states}}{{# transitions }} {{ from_state }} -> {{to_state}};
16
+ {{/ transitions }}{{/ states}}
17
+ {{/ data }}
18
+
19
+
20
+ }