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