tla2dot 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }