resyma 0.1.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +31 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +69 -0
  7. data/LICENSE +674 -0
  8. data/README.md +167 -0
  9. data/Rakefile +8 -0
  10. data/lib/resyma/core/algorithm/engine.rb +189 -0
  11. data/lib/resyma/core/algorithm/matcher.rb +48 -0
  12. data/lib/resyma/core/algorithm/tuple.rb +25 -0
  13. data/lib/resyma/core/algorithm.rb +5 -0
  14. data/lib/resyma/core/automaton/builder.rb +78 -0
  15. data/lib/resyma/core/automaton/definition.rb +32 -0
  16. data/lib/resyma/core/automaton/epsilon_NFA.rb +115 -0
  17. data/lib/resyma/core/automaton/matchable.rb +16 -0
  18. data/lib/resyma/core/automaton/regexp.rb +175 -0
  19. data/lib/resyma/core/automaton/state.rb +22 -0
  20. data/lib/resyma/core/automaton/transition.rb +58 -0
  21. data/lib/resyma/core/automaton/visualize.rb +23 -0
  22. data/lib/resyma/core/automaton.rb +9 -0
  23. data/lib/resyma/core/parsetree/builder.rb +89 -0
  24. data/lib/resyma/core/parsetree/converter.rb +61 -0
  25. data/lib/resyma/core/parsetree/default_converter.rb +331 -0
  26. data/lib/resyma/core/parsetree/definition.rb +77 -0
  27. data/lib/resyma/core/parsetree/source.rb +73 -0
  28. data/lib/resyma/core/parsetree/traversal.rb +26 -0
  29. data/lib/resyma/core/parsetree.rb +8 -0
  30. data/lib/resyma/core/utilities.rb +30 -0
  31. data/lib/resyma/language.rb +290 -0
  32. data/lib/resyma/nise/date.rb +53 -0
  33. data/lib/resyma/nise/rubymoji.rb +13 -0
  34. data/lib/resyma/nise/toml.rb +63 -0
  35. data/lib/resyma/parsetree.rb +163 -0
  36. data/lib/resyma/program/automaton.rb +84 -0
  37. data/lib/resyma/program/parsetree.rb +79 -0
  38. data/lib/resyma/program/traverse.rb +77 -0
  39. data/lib/resyma/version.rb +5 -0
  40. data/lib/resyma.rb +12 -0
  41. data/resyma.gemspec +47 -0
  42. data/sig/resyma.rbs +4 -0
  43. metadata +184 -0
@@ -0,0 +1,290 @@
1
+ require "parser"
2
+ require "resyma/parsetree"
3
+ require "resyma/core/automaton"
4
+ require "resyma/core/parsetree"
5
+ require "resyma/core/algorithm"
6
+
7
+ module Resyma
8
+ class IllegalLanguageDefinitionError < Error; end
9
+
10
+ class IllegalRegexError < Error; end
11
+
12
+ class LanguageSyntaxError < Error; end
13
+
14
+ #
15
+ # An visitor which builds automaton. The particular syntax is defined as
16
+ # follow:
17
+ #
18
+ # regex: char | seq | rep | or | opt
19
+ # char: STRING | ID | ID '(' STRING ')'
20
+ # seq: '(' regex (';' regex)+ ')'
21
+ # rep: regex '..' | regex '...'
22
+ # or: '[' regex (',' regex)+ ']'
23
+ # opt: '[' regex ']'
24
+ #
25
+ class RegexBuildVistor
26
+ #
27
+ # Build a Resyma::Core::Automaton from an AST
28
+ #
29
+ # @param [Parser::AST::Node] ast An abstract syntax tree
30
+ #
31
+ # @return [Resyma::Core::Regexp] A regular expression
32
+ #
33
+ def visit(ast)
34
+ case ast.type
35
+ when :array
36
+ if ast.children.length > 1
37
+ build_or ast
38
+ elsif ast.children.empty?
39
+ raise IllegalRegexError, "Empty array is illegal"
40
+ else
41
+ build_opt ast
42
+ end
43
+ when :begin
44
+ build_seq(ast)
45
+ when :irange
46
+ build_rep(ast, false)
47
+ when :erange
48
+ build_rep(ast, true)
49
+ when :str
50
+ value = ast.children.first
51
+ type = Core::CONST_TOKEN_TABLE[value]
52
+ if type.nil?
53
+ raise IllegalRegexError,
54
+ "Unknown constant token [#{value}]"
55
+ end
56
+ build_char(type, value)
57
+ when :send
58
+ rec, type, *args = ast.children
59
+ raise IllegalRegexError, "Reciever #{rec} is illegal" unless rec.nil?
60
+
61
+ if args.length > 1
62
+ raise IllegalRegexError,
63
+ "Two or more arguments is illegal"
64
+ end
65
+
66
+ return build_char(type, nil) if args.empty?
67
+
68
+ value = args.first
69
+ if value.type == :str
70
+ build_char(type, value.children.first)
71
+ else
72
+ raise IllegalRegexError,
73
+ "Character regex only accepts static string as value, got " +
74
+ value.type.to_s
75
+ end
76
+ end
77
+ end
78
+
79
+ include Core::RegexpOp
80
+ def build_char(type, value)
81
+ rchr(Core::PTNodeMatcher.new(type, value))
82
+ end
83
+
84
+ def build_seq(ast)
85
+ regex_lst = ast.children.map { |sub| visit(sub) }
86
+ rcat(*regex_lst)
87
+ end
88
+
89
+ def build_rep(ast, one_or_more)
90
+ left, right = ast.children
91
+ raise IllegalRegexError, "Beginless range is illegal" if left.nil?
92
+ raise IllegalRegexError, "Only endless range is legal" unless right.nil?
93
+
94
+ regex = visit(left)
95
+ if one_or_more
96
+ rcat(regex, rrep(regex))
97
+ else
98
+ rrep(regex)
99
+ end
100
+ end
101
+
102
+ def build_or(ast)
103
+ n = ast.children.length
104
+ unless n > 1
105
+ raise IllegalRegexError,
106
+ "Or-regex must contain two or more branches, but found #{n}"
107
+ end
108
+
109
+ regex_lst = ast.children.map { |sub| visit(sub) }
110
+ ror(*regex_lst)
111
+ end
112
+
113
+ def build_opt(ast)
114
+ value = ast.children[0]
115
+ regex = visit(value)
116
+ ror(reps, regex)
117
+ end
118
+ end
119
+
120
+ class ActionEnvironment
121
+ def initialize(nodes, binding, filename, lineno)
122
+ @nodes = nodes
123
+ @src_binding = binding
124
+ @src_filename = filename
125
+ @src_lineno = lineno
126
+ end
127
+
128
+ attr_reader :nodes, :src_binding, :src_filename, :src_lineno
129
+ end
130
+
131
+ #
132
+ # Language created from single automaton and a associated action. Syntax:
133
+ #
134
+ # regex >> expr
135
+ #
136
+ # where `regex` should adhere to syntax described in Resyma::RegexBuildVistor,
137
+ # and `expr` is an arbitrary ruby expression. Note that
138
+ #
139
+ # - Readonly variable `nodes` is visible in the `expr`, which is a `Array` of
140
+ # Resyma::Core::ParseTree and denotes the derivational node sequence
141
+ # - Readonly variables `src_binding`, `src_filename` and `src_lineno` are
142
+ # visible in the `expr`, which describe the environment surrounding the DSL
143
+ # - Variables above can be shadowed by local variables
144
+ # - Multiple expressions can be grouped to atom by `begin; end`
145
+ #
146
+ class MonoLanguage
147
+ def initialize(automaton, action)
148
+ @automaton = automaton
149
+ @action = action
150
+ end
151
+
152
+ attr_accessor :automaton, :action
153
+
154
+ def self.node(type, children)
155
+ Parser::AST::Node.new(type, children)
156
+ end
157
+
158
+ def self.from(ast, bd, filename, _lino)
159
+ if ast.type != :send
160
+ raise IllegalLanguageDefinitionError,
161
+ "AST with type #{ast} cannot define a language"
162
+ elsif ast.children[1] != :>>
163
+ raise IllegalLanguageDefinitionError,
164
+ "Only AST whose operator is '>>' can define a language"
165
+ elsif ast.children.length != 3
166
+ raise IllegalLanguageDefinitionError,
167
+ "Language definition should be 'regex >> expr'"
168
+ end
169
+
170
+ regex_ast, _, action_ast = ast.children
171
+
172
+ automaton = RegexBuildVistor.new.visit(regex_ast).to_automaton
173
+ action_proc_ast = node :block, [
174
+ node(:send, [nil, :lambda]),
175
+ node(:args, [node(:arg, [:__ae__])]),
176
+ node(:block, [
177
+ node(:send, [node(:lvar, [:__ae__]), :instance_eval]),
178
+ node(:args, []),
179
+ action_ast
180
+ ])
181
+ ]
182
+ action_str = Unparser.unparse(action_proc_ast)
183
+ # lambda generated by unparser will add two lines before our code
184
+ # The following adjustment allow us to locate `proc`s in the action
185
+ # properly
186
+ action = eval(action_str, bd, filename,
187
+ action_ast.loc.expression.line - 2)
188
+ new automaton, action
189
+ end
190
+ end
191
+
192
+ class Language
193
+ def initialize
194
+ # @type [Array<Resyma::MonoLanguage>]
195
+ @mono_languages = nil
196
+ # @type [Resyma::Core::Engine]
197
+ @engine = nil
198
+ end
199
+
200
+ attr_reader :engine
201
+
202
+ def syntax; end
203
+
204
+ def build_language(procedure)
205
+ ast, bd, filename, lino = Resyma.source_of(procedure)
206
+ body_ast = Resyma.extract_body(ast)
207
+ if body_ast.nil?
208
+ raise LanguageSyntaxError,
209
+ "Define your language by override method syntax"
210
+ end
211
+ if body_ast.type == :begin
212
+ @mono_languages = body_ast.children.map do |stm_ast|
213
+ MonoLanguage.from(stm_ast, bd, filename, lino)
214
+ end
215
+ @engine = Resyma::Core::Engine.new(@mono_languages.map(&:automaton))
216
+ else
217
+ @mono_languages = [MonoLanguage.from(body_ast, bd, filename, lino)]
218
+ @engine = Resyma::Core::Engine.new(@mono_languages.first.automaton)
219
+ end
220
+ end
221
+
222
+ def built?
223
+ @mono_languages && @engine
224
+ end
225
+
226
+ #
227
+ # Interpret the parse tree as current language. Note that the original
228
+ # evaluation result in the tree will be overlapped.
229
+ #
230
+ # @param [Resyma::Core::ParseTree] parsetree A parse tree
231
+ # @param [Binding] binding Environment
232
+ # @param [String] filename Source location
233
+ # @param [Integer] lineno Source location
234
+ #
235
+ # @return [Object] Evaluation result
236
+ #
237
+ def load_parsetree!(parsetree, binding, filename, lineno, need_clean = true)
238
+ build_language(method(:syntax)) unless built?
239
+ parsetree.clear! if need_clean
240
+ parsetree.parent = nil
241
+ @engine.traverse!(parsetree)
242
+ accepted_set = @engine.accepted_tuples(parsetree)
243
+ tuple4 = accepted_set.min_by(&:belongs_to)
244
+ if tuple4.nil?
245
+ raise LanguageSyntaxError,
246
+ "The code does not adhere to syntax defined by #{self.class}"
247
+ end
248
+ dns = @engine.backtrack_for(parsetree, tuple4)
249
+ nodes = dns.map { |t| @engine.node_of(parsetree, t.p_) }
250
+ action = @mono_languages[tuple4.belongs_to].action
251
+ ae = ActionEnvironment.new(nodes, binding, filename, lineno)
252
+ action.call(ae)
253
+ end
254
+
255
+ #
256
+ # Interpret the AST as current language
257
+ #
258
+ # @param [Parser::AST::Node] ast An abstract syntax tree
259
+ #
260
+ # @return [Object] Result returned by action, see Resyma::MonoLanguage for
261
+ # more information
262
+ #
263
+ def load_ast(ast, binding, filename, lineno)
264
+ parsetree = Core::DEFAULT_CONVERTER.convert(ast)
265
+ load_parsetree!(parsetree, binding, filename, lineno, false)
266
+ end
267
+
268
+ #
269
+ # Load a block as DSL. Note that argument of the block will be ignored.
270
+ #
271
+ # @return [Object] Result of the evaluation, defined by current DSL
272
+ #
273
+ def load(&block)
274
+ ast, bd, filename, = Resyma.source_of(block)
275
+ body_ast = Resyma.extract_body(ast)
276
+ lino = body_ast.loc.expression.line
277
+ load_ast(body_ast, bd, filename, lino)
278
+ end
279
+
280
+ #
281
+ # Initialize a new instance without argument and call `#load`
282
+ #
283
+ # @return [Object] Result of the evaluation.
284
+ # @see See #load
285
+ #
286
+ def self.load(&block)
287
+ new.load(&block)
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,53 @@
1
+ require "resyma"
2
+ require "date"
3
+
4
+ #
5
+ # DSL reading dates
6
+ #
7
+ class LangDate < Resyma::Language
8
+ def syntax
9
+ id("today") >> Date.today
10
+
11
+ (int; id("/"); int; id("/"); int) >> begin
12
+ year = nodes[0].to_ruby
13
+ month = nodes[2].to_ruby
14
+ day = nodes[4].to_ruby
15
+ Date.new(year, month, day)
16
+ end
17
+
18
+ (numop; numbase; "."; [id("day"), id("month"), id("year")]) >> begin
19
+ op, num, _, unit = nodes
20
+ sig = op.to_literal == "+" ? 1 : -1
21
+ val = num.to_literal.to_i * sig
22
+ case unit.to_literal
23
+ when "day" then Date.today.next_day(val)
24
+ when "month" then Date.today.next_month(val)
25
+ when "year" then Date.today.next_year(val)
26
+ end
27
+ end
28
+
29
+ id("yesterday") >> LangDate.load { -1.day }
30
+ id("tomorrow") >> LangDate.load { +1.day }
31
+ end
32
+ end
33
+
34
+ def date(&block)
35
+ LangDate.load(&block)
36
+ end
37
+
38
+ class LangTimeline < Resyma::Language
39
+ def syntax
40
+ (array; id("-"); str)... >> begin
41
+ items = []
42
+ until nodes.empty?
43
+ date, _, text = (1..3).map { nodes.shift }
44
+ raise SyntaxError if date.children.length != 3 # '[' DATE ']'
45
+
46
+ date = LangDate.new.load_parsetree!(date.children[1], src_binding,
47
+ src_filename, src_lineno)
48
+ items.push [date, text.to_ruby]
49
+ end
50
+ items
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ require "resyma"
2
+
3
+ class LangRubymoji < Resyma::Language
4
+ def syntax
5
+ (id("O"); "."; id("O"); str("??")) >> "🤔"
6
+ (id("o"); id("^"); id("o")) >> "🙃"
7
+ (id("Zzz"); ".."; "("; id("x"); "."; id("x"); ")") >> "😴"
8
+ end
9
+ end
10
+
11
+ def rumoji(&block)
12
+ LangRubymoji.load(&block)
13
+ end
@@ -0,0 +1,63 @@
1
+ require "resyma"
2
+
3
+ class LangTOMLBuilder
4
+ def initialize
5
+ @root = make_hash
6
+ @prefix = []
7
+ end
8
+
9
+ def make_hash
10
+ Hash.new { |hash, key| hash[key] = make_hash }
11
+ end
12
+
13
+ attr_accessor :root, :prefix
14
+
15
+ def add!(path, value)
16
+ raise SyntaxError if path.empty?
17
+ abs_path = @prefix + path
18
+ cur = @root
19
+ abs_path[...-1].each do |name|
20
+ cur = cur[name.to_sym]
21
+ end
22
+ cur[abs_path.last.to_sym] = value
23
+ end
24
+ end
25
+
26
+ class LangTOMLNamespace < Resyma::Language
27
+ def syntax
28
+ ("["; id; ("."; id)..; "]") >> begin
29
+ nodes.select { |n| n.symbol == :id }.map { |n| n.to_literal }
30
+ end
31
+ end
32
+ end
33
+
34
+ class LangTOML < Resyma::Language
35
+ def syntax
36
+ [array,
37
+ (id; ("."; id)..; "="; [int, str, "true", "false", array, hash])]... >>
38
+ begin
39
+ bdr = LangTOMLBuilder.new
40
+ until nodes.empty?
41
+ car = nodes.shift
42
+ if car.symbol == :array
43
+ namespace =
44
+ LangTOMLNamespace.new.load_parsetree!(
45
+ car, src_binding, src_filename, src_lineno
46
+ )
47
+ bdr.prefix = namespace
48
+ else
49
+ path = [car]
50
+ car = nodes.shift
51
+ until car.symbol == :eq
52
+ path.push car if car.symbol == :id
53
+ car = nodes.shift
54
+ end
55
+ value = nodes.shift
56
+ bdr.add!(path.map(&:to_literal),
57
+ value.to_ruby(src_binding, src_filename, src_lineno))
58
+ end
59
+ end
60
+ bdr.root
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,163 @@
1
+ require "resyma/core/parsetree"
2
+ require "unparser"
3
+
4
+ module Resyma
5
+ class UnsupportedError < Error; end
6
+ class NoASTError < Error; end
7
+
8
+ #
9
+ # Derive an AST from a procedure-like object
10
+ #
11
+ # @param [#to_proc] procedure Typically Proc or Method
12
+ #
13
+ # @return [[Parser::AST::Node, Binding, filename, lino]] An abstract syntax
14
+ # tree, the environment surrounding the procedure, and its source location
15
+ #
16
+ def self.source_of(procedure)
17
+ procedure = procedure.to_proc
18
+ ast = Core.locate(procedure)
19
+ raise UnsupportedError, "Cannot locate the source of #{ast}" if ast.nil?
20
+
21
+ [ast, procedure.binding, *procedure.source_location]
22
+ end
23
+
24
+ #
25
+ # Extract body part from AST of a procedure-like object
26
+ #
27
+ # @param [Parser::AST::Node] procedure_ast AST of a Proc or a Method
28
+ #
29
+ # @return [Parser::AST::Node] AST of function body
30
+ #
31
+ def self.extract_body(procedure_ast)
32
+ case procedure_ast.type
33
+ when :block then procedure_ast.children[2]
34
+ when :def then procedure_ast.children[2]
35
+ when :defs then procedure_ast.children[3]
36
+ else
37
+ raise UnsupportedError,
38
+ "Not a supported type of procedure: #{procedure_ast.type}"
39
+ end
40
+ end
41
+
42
+ #
43
+ # Derive the parse tree of a function body
44
+ #
45
+ # @param [#to_proc] procedure A procedure-like object, typically Proc and
46
+ # Method
47
+ #
48
+ # @return [[Resyma::Core::ParseTree, Binding, filename, lino]] A parse tree,
49
+ # the environment surrounding the procedure, and its source location
50
+ #
51
+ def self.body_parsetree_of(procedure)
52
+ ast, bd, filename, lino = source_of(procedure)
53
+ body_ast = extract_body(ast)
54
+ [Core::DEFAULT_CONVERTER.convert(body_ast), bd, filename, lino]
55
+ end
56
+
57
+ #
58
+ # Evaluator for Resyma::Core::ParseTree
59
+ #
60
+ class Evaluator
61
+ def initialize
62
+ @rules = {}
63
+ end
64
+
65
+ #
66
+ # Define a evaluation rule
67
+ #
68
+ # @param [Symbol, Array<Symbol>] type Type(s) assocating to the rule
69
+ # @yieldparam [Parser::AST::Node] AST of the node
70
+ # @yieldparam [Binding] The environment surrounding the DSL
71
+ # @yieldparam [String] filename Source location
72
+ # @yieldparam [Integer] lino Source location
73
+ #
74
+ # @return [nil] Nothing
75
+ #
76
+ def def_rule(type, &block)
77
+ types = type.is_a?(Array) ? type : [type]
78
+ types.each { |sym| @rules[sym] = block }
79
+ nil
80
+ end
81
+
82
+ #
83
+ # Evaluate AST of the parse tree
84
+ #
85
+ # @param [Resyma::Core::ParseTree] parsetree A parse tree whose `ast` is not
86
+ # `nil`
87
+ # @param [Binding] bd Environment
88
+ # @param [String] filename Source location
89
+ # @param [Integer] lino Source location
90
+ #
91
+ # @return [Object] Reterning value of corresponding evaluating rule
92
+ #
93
+ def evaluate(parsetree, bd, filename, lino)
94
+ if parsetree.ast.nil?
95
+ raise NoASTError,
96
+ "AST of parse trees is necessary for evaluation"
97
+ end
98
+
99
+ evaluate_ast(parsetree.ast, bd, filename, lino)
100
+ end
101
+
102
+ #
103
+ # Evaluate the AST by defined rules
104
+ #
105
+ # @param [Parser::AST::Node] ast An abstract syntax tree
106
+ # @param [Binding] bd Environment
107
+ # @param [String] filename Source location
108
+ # @param [Integer] lino Source location
109
+ #
110
+ # @return [Object] Returning value of corresponding evaluating rule
111
+ #
112
+ def evaluate_ast(ast, bd, filename, lino)
113
+ evaluator = @rules[ast.type]
114
+ if evaluator.nil?
115
+ fallback ast, bd, filename, lino
116
+ else
117
+ evaluator.call(ast, bd, filename, lino)
118
+ end
119
+ end
120
+
121
+ #
122
+ # Fallback evaluating method. AST whose type is not defined by current
123
+ # evaluator will be passed to this method. The default action is unparse
124
+ # the AST by `unparser` and evaluate the string by `eval`
125
+ #
126
+ # @param [Parser::AST::Node] ast An abstract syntax tree
127
+ # @param [Binding] bd The environment
128
+ # @param [String] filename Source location
129
+ # @param [Integer] lino Source location
130
+ #
131
+ # @return [Object] Evaluating result
132
+ #
133
+ def fallback(ast, bd, filename, lino)
134
+ string = Unparser.unparse(ast)
135
+ eval(string, bd, filename, lino)
136
+ end
137
+ end
138
+
139
+ module Core
140
+ class ParseTree
141
+ #
142
+ # Evaluate current parse tree using default evaluator
143
+ #
144
+ # @param [Binding] bd Environment
145
+ # @param [String] filename Source location
146
+ # @param [Integer] lino Source location
147
+ #
148
+ # @return [Object] Evaluation result
149
+ #
150
+ def to_ruby(bd = binding, filename = "(resyma)", lino = 1)
151
+ Evaluator.new.evaluate(self, bd, filename, lino)
152
+ end
153
+
154
+ def to_literal
155
+ unless leaf?
156
+ raise TypeError,
157
+ "Cannot convert a non-leaf node(i.e. non-token) to literal"
158
+ end
159
+ children.first
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,84 @@
1
+ require "resyma"
2
+ require "ruby-graphviz"
3
+ require "resyma/core/automaton"
4
+ require "parser/current"
5
+
6
+ class Visualizer
7
+ def initialize(automaton)
8
+ @viz = GraphViz.new(:G, type: :graph)
9
+ @viz[:rankdir] = "LR"
10
+ @id_pool = 0
11
+ # @type [Resyma::Core::Automaton]
12
+ @automaton = automaton
13
+ @node_cache = {}
14
+ end
15
+
16
+ def def_state(state)
17
+ @id_pool += 1
18
+ label = state.id.to_s
19
+ shape = @automaton.accept?(state) ? "doublecircle" : "circle"
20
+ @viz.add_node(@id_pool.to_s, label: label, shape: shape)
21
+ end
22
+
23
+ def shorten(str, limit = 10)
24
+ str = str.to_s
25
+ if str.length > limit
26
+ str[0...limit] + "..."
27
+ else
28
+ str
29
+ end
30
+ end
31
+
32
+ def node_of(state)
33
+ node = @node_cache[state]
34
+ return node if node
35
+
36
+ node = def_state(state)
37
+ @node_cache[state] = node
38
+ node
39
+ end
40
+
41
+ def viz_matcher(node_matcher)
42
+ rez = node_matcher.type.to_s
43
+ rez += "(#{shorten(node_matcher.value)})" if node_matcher.value
44
+ rez
45
+ end
46
+
47
+ def viz!
48
+ @automaton.transition_table.table.each do |src, value|
49
+ src_node = node_of src
50
+ value.each do |can|
51
+ node_matcher = can.condition
52
+ dest = can.destination
53
+ dest_node = node_of(dest)
54
+ @viz.add_edge(src_node, dest_node, { label: viz_matcher(node_matcher) })
55
+ end
56
+ end
57
+ end
58
+
59
+ def output(filename)
60
+ @viz.output(png: filename)
61
+ end
62
+ end
63
+
64
+ def launch
65
+ output_filename = "./resyma-automaton.png"
66
+
67
+ OptionParser.new do |opts|
68
+ opts.on("-o", "--output FILE",
69
+ "PNG containing the resulting tree") do |file|
70
+ output_filename = file
71
+ end
72
+ opts.on_tail("-h", "--help", "Show this message") do
73
+ puts opts
74
+ exit
75
+ end
76
+ end.parse!
77
+
78
+ source = $stdin.read
79
+ ast = Parser::CurrentRuby.parse(source)
80
+ automaton = Resyma::RegexBuildVistor.new.visit(ast).to_automaton
81
+ viz = Visualizer.new(automaton)
82
+ viz.viz!
83
+ viz.output(output_filename)
84
+ end