resyma 0.1.1

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