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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +69 -0
- data/LICENSE +674 -0
- data/README.md +167 -0
- data/Rakefile +8 -0
- data/lib/resyma/core/algorithm/engine.rb +189 -0
- data/lib/resyma/core/algorithm/matcher.rb +48 -0
- data/lib/resyma/core/algorithm/tuple.rb +25 -0
- data/lib/resyma/core/algorithm.rb +5 -0
- data/lib/resyma/core/automaton/builder.rb +78 -0
- data/lib/resyma/core/automaton/definition.rb +32 -0
- data/lib/resyma/core/automaton/epsilon_NFA.rb +115 -0
- data/lib/resyma/core/automaton/matchable.rb +16 -0
- data/lib/resyma/core/automaton/regexp.rb +175 -0
- data/lib/resyma/core/automaton/state.rb +22 -0
- data/lib/resyma/core/automaton/transition.rb +58 -0
- data/lib/resyma/core/automaton/visualize.rb +23 -0
- data/lib/resyma/core/automaton.rb +9 -0
- data/lib/resyma/core/parsetree/builder.rb +89 -0
- data/lib/resyma/core/parsetree/converter.rb +61 -0
- data/lib/resyma/core/parsetree/default_converter.rb +331 -0
- data/lib/resyma/core/parsetree/definition.rb +77 -0
- data/lib/resyma/core/parsetree/source.rb +73 -0
- data/lib/resyma/core/parsetree/traversal.rb +26 -0
- data/lib/resyma/core/parsetree.rb +8 -0
- data/lib/resyma/core/utilities.rb +30 -0
- data/lib/resyma/language.rb +290 -0
- data/lib/resyma/nise/date.rb +53 -0
- data/lib/resyma/nise/rubymoji.rb +13 -0
- data/lib/resyma/nise/toml.rb +63 -0
- data/lib/resyma/parsetree.rb +163 -0
- data/lib/resyma/program/automaton.rb +84 -0
- data/lib/resyma/program/parsetree.rb +79 -0
- data/lib/resyma/program/traverse.rb +77 -0
- data/lib/resyma/version.rb +5 -0
- data/lib/resyma.rb +12 -0
- data/resyma.gemspec +47 -0
- data/sig/resyma.rbs +4 -0
- 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
|