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
data/README.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
# A DSL Engine
|
2
|
+
|
3
|
+
## Introduction
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
require "resyma"
|
7
|
+
require "date"
|
8
|
+
|
9
|
+
#
|
10
|
+
# Define a new language by creating a subclass of Resyma::Language and
|
11
|
+
# specifying the syntax in method 'syntax'
|
12
|
+
#
|
13
|
+
class LangDate < Resyma::Language
|
14
|
+
# syntax of 'syntax': regex >> action
|
15
|
+
def syntax
|
16
|
+
# e.g. today
|
17
|
+
id("today") >> Date.today
|
18
|
+
|
19
|
+
# e.g. 2023/1/1
|
20
|
+
(int; id("/"); int; id("/"); int) >> begin
|
21
|
+
year = nodes[0].to_ruby
|
22
|
+
month = nodes[2].to_ruby
|
23
|
+
day = nodes[4].to_ruby
|
24
|
+
Date.new(year, month, day)
|
25
|
+
end
|
26
|
+
|
27
|
+
# e.g. +1.year
|
28
|
+
(numop; numbase; "."; [id("day"), id("month"), id("year")]) >> begin
|
29
|
+
op, num, _, unit = nodes
|
30
|
+
sig = op.to_literal == "+" ? 1 : -1
|
31
|
+
val = num.to_literal.to_i * sig
|
32
|
+
case unit.to_literal
|
33
|
+
when "day" then Date.today.next_day(val)
|
34
|
+
when "month" then Date.today.next_month(val)
|
35
|
+
when "year" then Date.today.next_year(val)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Recursively interpret
|
40
|
+
id("yesterday") >> LangDate.load { -1.day }
|
41
|
+
id("tomorrow") >> LangDate.load { +1.day }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def date(&block)
|
46
|
+
LangDate.load(&block) # Interpret a block as DSL
|
47
|
+
end
|
48
|
+
|
49
|
+
date { today } #=> #<Date: 2023-02-09 (...)>
|
50
|
+
date { tomorrow } #=> #<Date: 2023-02-10 (...)>
|
51
|
+
date { 2024/2/9 } #=> #<Date: 2024-02-09 (...)>
|
52
|
+
date { +7.day } #=> #<Date: 2023-02-16 (...)>
|
53
|
+
date { -3.month } #=> #<Date: 2022-11-09 (...)>
|
54
|
+
```
|
55
|
+
|
56
|
+
`Resyma` is a draft of a DSL engine. We prevent blocks containing DSL from evaluating, apply our matching algorithm to the parse tree, and pass matched nodes to libraries to implement the specific semantics of their DSL. Since semantic restrictions like method definiton are unimportant, the syntax of your DSL can be quite free.
|
57
|
+
|
58
|
+
Note that this library is unstable and experimental. Several severe limitations will be described in following sections.
|
59
|
+
|
60
|
+
## Define your DSL
|
61
|
+
|
62
|
+
Define a new DSL by defining a subclass of `Resyma::Language`.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class MyLang << Resyma::Language
|
66
|
+
def syntax
|
67
|
+
regex >> action
|
68
|
+
more...
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
`regex` is a DSL defining syntax of your language, and `action` is an arbitrary ruby expression defining semantics of your language. In particular, `regex` is one of following:
|
74
|
+
|
75
|
+
- `type`, `type("value")`, `"value"`: match a node by type, value, or both.
|
76
|
+
- `(a; b; c)`: match a sequence of nodes in order of `a`, `b`, `c`, where every component is a `regex`
|
77
|
+
- `[a, b, c]`: match one of `a`, `b`, `c`, where every component is a `regex`
|
78
|
+
- `a..`, `a...`: match `a` zero or more time, or one or more time, where `a` is a `regex`
|
79
|
+
- `[a]`: optionally match `a`
|
80
|
+
|
81
|
+
Comprehensive document is in the plan.
|
82
|
+
|
83
|
+
## Limitations
|
84
|
+
|
85
|
+
- Parse tree, not AST
|
86
|
+
Our algorithm works on parse trees, namely concrete syntax trees, but not AST. However, most of Ruby libraries function only at the AST level. Currently, we derive AST by [parser](https://github.com/whitequark/parser) and convert it to parse tree. It is an unacceptable solution because AST of `parser` describes abstract structures of codes and disregards details like parenthesis or semicolons, which in turn causes malfunction of our algorithm.
|
87
|
+
- Capturing group
|
88
|
+
In regular expression of string, we capture key components by grouping (e.g., `/Hi, (\w+)!/`) for further processing. Without this feature, regular expression is just a boolean function and almost useless. Currently, `Resyma` does not support capturing group, but we can provide users with a complete list of nodes matched with the regular expression. So users can process matched nodes but cannot choose specific nodes.
|
89
|
+
|
90
|
+
## More examples
|
91
|
+
|
92
|
+
### Nise-TOML
|
93
|
+
|
94
|
+
[TOML](https://toml.io/en/) is a configuraton language.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
require "resyma/nise/toml"
|
98
|
+
|
99
|
+
LangTOML.load do
|
100
|
+
|
101
|
+
# This is a nise-TOML document
|
102
|
+
|
103
|
+
title = "TOML Example"
|
104
|
+
|
105
|
+
[owner]
|
106
|
+
name = "Tom Preston-Werner"
|
107
|
+
|
108
|
+
[database]
|
109
|
+
enabled = true
|
110
|
+
ports = [ 8000, 8001, 8002 ]
|
111
|
+
data = [ ["delta", "phi"], [3.14] ]
|
112
|
+
temp_targets = { cpu: 79.5, case: 72.0 }
|
113
|
+
|
114
|
+
[servers]
|
115
|
+
|
116
|
+
[servers.alpha]
|
117
|
+
ip = "10.0.0.1"
|
118
|
+
role = "frontend"
|
119
|
+
|
120
|
+
[servers.beta]
|
121
|
+
ip = "10.0.0.2"
|
122
|
+
role = "backend"
|
123
|
+
end
|
124
|
+
|
125
|
+
#=> {:title => "TOML Example",
|
126
|
+
# :owner => {:name => "Tom Preston-Werner"},
|
127
|
+
# :database => {:enabled => true,
|
128
|
+
# :ports => [8000, 8001, 8002],
|
129
|
+
# :data => [["delta", "phi"], [3.14]],
|
130
|
+
# :temp_targets =>{:cpu => 79.5, :case => 72.0}},
|
131
|
+
# :servers => {:alpha => {:ip => "10.0.0.1", :role => "frontend"},
|
132
|
+
# :beta => {:ip => "10.0.0.2", :role => "backend"}}}
|
133
|
+
```
|
134
|
+
|
135
|
+
### Timeline
|
136
|
+
|
137
|
+
`Timeline` uses DSL defined by the example at the top.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
require "resyma/nise/date"
|
141
|
+
|
142
|
+
LangTimeline.load do
|
143
|
+
[2020/8/15] - "First day of class"
|
144
|
+
[2020/10/9] - "Test #1"
|
145
|
+
[yesterday] - "Research paper due"
|
146
|
+
[today] - "Zzz..."
|
147
|
+
[+7.day] - "Test #2"
|
148
|
+
[+2.month] - "Final project due"
|
149
|
+
end
|
150
|
+
|
151
|
+
#=> [[#<Date: 2020-08-15 (...)>, "First day of class"],
|
152
|
+
# [#<Date: 2020-10-09 (...)>, "Test #1"],
|
153
|
+
# [#<Date: 2023-02-08 (...)>, "Research paper due"],
|
154
|
+
# [#<Date: 2023-02-09 (...)>, "Zzz..."],
|
155
|
+
# [#<Date: 2023-02-16 (...)>, "Test #2"],
|
156
|
+
# [#<Date: 2023-04-09 (...)>, "Final project due"]]
|
157
|
+
```
|
158
|
+
|
159
|
+
### Rubymoji
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
require "resyma/nise/rubymoji"
|
163
|
+
|
164
|
+
rumoji { o^o } #=> 🙃
|
165
|
+
rumoji { O.O ?? } #=> 🤔
|
166
|
+
rumoji { Zzz.. (x.x) } #=> 😴
|
167
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require "resyma/core/utilities"
|
2
|
+
require "resyma/core/automaton"
|
3
|
+
require "resyma/core/algorithm/tuple"
|
4
|
+
|
5
|
+
module Resyma
|
6
|
+
module Core
|
7
|
+
#
|
8
|
+
# The engine of the matching algorithm
|
9
|
+
#
|
10
|
+
class Engine
|
11
|
+
#
|
12
|
+
# Create an instance of the algorithmic engine
|
13
|
+
#
|
14
|
+
# @param [Array<Resyma::Core::Automaton>, Resyma::Core::Automaton]
|
15
|
+
# automata A list of automata
|
16
|
+
#
|
17
|
+
def initialize(automata)
|
18
|
+
automata = [automata] if automata.is_a?(Automaton)
|
19
|
+
raise TypeError, "Need a list of automata" unless automata.is_a?(Array)
|
20
|
+
|
21
|
+
@automata = automata
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Determine the type of the node corresponding step 2 of the algorithm
|
26
|
+
#
|
27
|
+
# @param [Resyma::Core::ParseTree] parsetree A node of parse tree
|
28
|
+
#
|
29
|
+
# @return [:a, :b, :c] Type of the node
|
30
|
+
#
|
31
|
+
def node_type(parsetree)
|
32
|
+
if parsetree.root? then :a
|
33
|
+
elsif parsetree.index.zero? then :b
|
34
|
+
else
|
35
|
+
:c
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Right-most path of the tree
|
41
|
+
#
|
42
|
+
# @param [Resyma::Core::ParseTree] parsetree A parse tree
|
43
|
+
#
|
44
|
+
# @return [Set<Resyma::Core::ParseTree>] A set of nodes on the RMP
|
45
|
+
#
|
46
|
+
def RMP(parsetree)
|
47
|
+
if parsetree.leaf?
|
48
|
+
Set[parsetree]
|
49
|
+
else
|
50
|
+
Set[parsetree] | RMP(parsetree.children.last)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Step 2 of the algorithm
|
56
|
+
#
|
57
|
+
# @param [Resyma::Core::ParseTree] node A parse tree node
|
58
|
+
#
|
59
|
+
# @return [nil] Nothing
|
60
|
+
#
|
61
|
+
def assign_start!(node)
|
62
|
+
@automata.each_with_index do |automaton, idx|
|
63
|
+
node.field.start[idx] =
|
64
|
+
case node_type(node)
|
65
|
+
when :a
|
66
|
+
Set[Tuple2.new(-1, automaton.start, belongs_to: idx)]
|
67
|
+
when :b
|
68
|
+
node.parent.field.start[idx]
|
69
|
+
when :c
|
70
|
+
# @type [Resyma::Core::ParseTree]
|
71
|
+
brother = node.parent.children[node.index - 1]
|
72
|
+
Utils.big_union(RMP(brother).map do |node_|
|
73
|
+
node_.field.trans[idx].map do |tuple4|
|
74
|
+
Tuple2.new(tuple4.p_, tuple4.q_, belongs_to: idx)
|
75
|
+
end
|
76
|
+
end)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Step 3 of the algorithm
|
83
|
+
#
|
84
|
+
# @param [Resyma::Core::ParseTree] node A parse tree
|
85
|
+
#
|
86
|
+
# @return [nil] Nothing
|
87
|
+
#
|
88
|
+
def assign_trans!(node)
|
89
|
+
@automata.each_with_index do |automaton, idx|
|
90
|
+
node.field.trans[idx] = node.field.start[idx].map do |tuple2|
|
91
|
+
next_q = automaton.destination(tuple2.q, node)
|
92
|
+
next_q and Tuple4.new(tuple2.p, tuple2.q, node.field.id, next_q,
|
93
|
+
belongs_to: idx)
|
94
|
+
end.compact.to_set
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
CACHE_INDEX_TO_NODE = "ALGO_IDX_NODE_MAP".freeze
|
99
|
+
|
100
|
+
#
|
101
|
+
# Find the parse tree through its ID
|
102
|
+
#
|
103
|
+
# @param [Resyma::Core::ParseTree] parsetree A parse tree processed by
|
104
|
+
# `#traverse!`
|
105
|
+
# @param [Integer] id Depth-first ordered ID, based on 0
|
106
|
+
#
|
107
|
+
# @return [Resyma::Core::ParseTree, nil] Result, or nil if node with `id`
|
108
|
+
# does not exist
|
109
|
+
#
|
110
|
+
def node_of(parsetree, id)
|
111
|
+
parsetree.cache[Engine::CACHE_INDEX_TO_NODE][id]
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Traverse the parse tree once and modify fields for every nodes
|
116
|
+
#
|
117
|
+
# @param [Resyma::Core::ParseTree] parsetree A parse tree
|
118
|
+
#
|
119
|
+
# @return [nil] Nothing
|
120
|
+
#
|
121
|
+
def traverse!(parsetree)
|
122
|
+
id = 0
|
123
|
+
parsetree.cache[Engine::CACHE_INDEX_TO_NODE] = {}
|
124
|
+
parsetree.depth_first_each do |tree|
|
125
|
+
tree.field.id = id
|
126
|
+
assign_start! tree
|
127
|
+
assign_trans! tree
|
128
|
+
parsetree.cache[Engine::CACHE_INDEX_TO_NODE][id] = tree
|
129
|
+
id += 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Compute accepted 4-tuples in the processed tree
|
135
|
+
#
|
136
|
+
# @param [Resyma::Core::ParseTree] parsetree A parse tree processed by
|
137
|
+
# `#traverse!`
|
138
|
+
#
|
139
|
+
# @return [Set<Resyma::Core::Tuple4>] A set of 4-tuples
|
140
|
+
#
|
141
|
+
def accepted_tuples(parsetree)
|
142
|
+
Utils.big_union(RMP(parsetree).map do |node|
|
143
|
+
Utils.big_union(node.field.trans.values.map do |set_of_tuple4|
|
144
|
+
set_of_tuple4.select do |tuple4|
|
145
|
+
@automata[tuple4.belongs_to].accept?(tuple4.q_)
|
146
|
+
end.to_set
|
147
|
+
end)
|
148
|
+
end)
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Backtrack the derivational node sequence terminating at the 4-tuple
|
153
|
+
#
|
154
|
+
# @param [Resyma::Core::ParseTree] parsetree A processed parse tree
|
155
|
+
# @param [Resyma::Core::Tuple4] accepted_tuple4 A 4-tuple derived from
|
156
|
+
# `#accepted_tuples`
|
157
|
+
#
|
158
|
+
# @return [Array<Resyma::Core::Tuple4>] A derivational node sequence
|
159
|
+
#
|
160
|
+
def backtrack_for(parsetree, tuple4)
|
161
|
+
if tuple4.p == -1
|
162
|
+
[tuple4]
|
163
|
+
else
|
164
|
+
# @type [Resyma::Core::ParseTree]
|
165
|
+
prev = parsetree.cache[Engine::CACHE_INDEX_TO_NODE][tuple4.p]
|
166
|
+
prev_tuple4 = prev.field.trans[tuple4.belongs_to].find do |candidate|
|
167
|
+
candidate.p_ == tuple4.p && candidate.q_ == tuple4.q
|
168
|
+
end
|
169
|
+
backtrack_for(parsetree, prev_tuple4) + [tuple4]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Backtrack the derivational node sequence(s) of a parse tree processed by
|
175
|
+
# the algorithm
|
176
|
+
#
|
177
|
+
# @param [Resyma::Core::ParseTree] parsetree A parse tree processed by
|
178
|
+
# `#traverse!`
|
179
|
+
# @param [Set<Resyma::Core::Tuple4>] terminal_tuples Accepted sets derived
|
180
|
+
# from `#accepted_tuples`
|
181
|
+
#
|
182
|
+
# @return [Array<Array<Resyma::Core::Tuple4>] Derivational node sequences
|
183
|
+
#
|
184
|
+
def backtrack(parsetree, terminal_tuples = accepted_tuples(parsetree))
|
185
|
+
terminal_tuples.map { |tuple4| backtrack_for(parsetree, tuple4) }
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "resyma/core/automaton/matchable"
|
2
|
+
|
3
|
+
module Resyma
|
4
|
+
module Core
|
5
|
+
class PTNodeMatcher
|
6
|
+
include Matchable
|
7
|
+
#
|
8
|
+
# Create a instance of parse tree node matcher
|
9
|
+
#
|
10
|
+
# @param [Symbol] type Symbol of the node
|
11
|
+
# @param [Object] value Value of the node, indicating that the node is a
|
12
|
+
# token. Currently, `nil` means that the node is an non-leaf node or it
|
13
|
+
# is a leaf node but its value is unimportant
|
14
|
+
#
|
15
|
+
def initialize(type, value = nil)
|
16
|
+
@type = type
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :type, :value
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
other.is_a?(PTNodeMatcher) &&
|
24
|
+
other.type == @type &&
|
25
|
+
other.value == @value
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Whether the matcher matches with the parse tree
|
30
|
+
#
|
31
|
+
# @param [Resyma::Core::ParseTree] parsetree Node of parse tree
|
32
|
+
#
|
33
|
+
# @return [true, false] Result
|
34
|
+
#
|
35
|
+
def match_with_value?(parsetree)
|
36
|
+
if parsetree.is_a?(Resyma::Core::ParseTree)
|
37
|
+
parsetree.symbol == @type &&
|
38
|
+
(@value.nil? || (parsetree.leaf? &&
|
39
|
+
parsetree.children[0] == @value))
|
40
|
+
elsif parsetree.is_a?(PTNodeMatcher)
|
41
|
+
self == parsetree
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Resyma
|
2
|
+
module Core
|
3
|
+
class Tuple4
|
4
|
+
def initialize(p, q, p_, q_, belongs_to: 0)
|
5
|
+
@p = p
|
6
|
+
@q = q
|
7
|
+
@p_ = p_
|
8
|
+
@q_ = q_
|
9
|
+
@belongs_to = belongs_to
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :p, :q, :p_, :q_, :belongs_to
|
13
|
+
end
|
14
|
+
|
15
|
+
class Tuple2
|
16
|
+
def initialize(p, q, belongs_to: 0)
|
17
|
+
@p = p
|
18
|
+
@q = q
|
19
|
+
@belongs_to = belongs_to
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :p, :q, :belongs_to
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "resyma/core/automaton/state"
|
4
|
+
require "resyma/core/automaton/transition"
|
5
|
+
|
6
|
+
module Resyma
|
7
|
+
module Core
|
8
|
+
class AutomatonBuilder
|
9
|
+
class NoStartError < Resyma::Error; end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@next_id = 0
|
13
|
+
@start = nil
|
14
|
+
@accept_set = Set[]
|
15
|
+
@transition_table = TransitionTable.new
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Adds a new state to the automaton and returns it
|
20
|
+
#
|
21
|
+
# @return [Resyma::Core::State] A new state
|
22
|
+
#
|
23
|
+
def new_state!(start: false, accept: false)
|
24
|
+
inst = State.with_id(@next_id)
|
25
|
+
@next_id += 1
|
26
|
+
start! inst if start
|
27
|
+
accept! inst if accept
|
28
|
+
inst
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Adds a new transition to the automaton
|
33
|
+
#
|
34
|
+
# @param [Resyma::Core::State] from_state Starting state
|
35
|
+
# @param [Resyma::Core::Matchable] matchable Condition
|
36
|
+
# @param [Resyma::Core::State] to_state Destination
|
37
|
+
#
|
38
|
+
# @return [nil] Nothing
|
39
|
+
#
|
40
|
+
def add_transition!(from_state, matchable, to_state)
|
41
|
+
@transition_table.add_transition!(from_state, matchable, to_state)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Specify a starting state for the automaton
|
46
|
+
#
|
47
|
+
# @param [Resyma::Core::State] state The starting state
|
48
|
+
#
|
49
|
+
# @return [nil] Nothing
|
50
|
+
#
|
51
|
+
def start!(state)
|
52
|
+
@start = state
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Add a state to the accept set of the automaton
|
58
|
+
#
|
59
|
+
# @param [Resyma::Core::State] state An acceptable state
|
60
|
+
#
|
61
|
+
# @return [nil] Nothing
|
62
|
+
#
|
63
|
+
def accept!(state)
|
64
|
+
@accept_set.add(state)
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def build
|
69
|
+
if @start.nil?
|
70
|
+
raise NoStartError,
|
71
|
+
"Cannot build a automaton without a start state"
|
72
|
+
end
|
73
|
+
|
74
|
+
Automaton.new(@start, @accept_set, @transition_table)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Resyma
|
4
|
+
module Core
|
5
|
+
class Automaton
|
6
|
+
#
|
7
|
+
# Returns a new instance of Resyma::Core::Automaton
|
8
|
+
#
|
9
|
+
# @param [Resyma::Core::State] start Starting state of the automaton
|
10
|
+
# @param [Set<Resyma::Core::State>] accept_set A set of states accepted by
|
11
|
+
# the automaton
|
12
|
+
# @param [Resyma::Core::TransitionTable] transition_table Transitions of
|
13
|
+
# the automaton
|
14
|
+
#
|
15
|
+
def initialize(start, accept_set, transition_table = TransitionTable.new)
|
16
|
+
@start = start
|
17
|
+
@accept_set = accept_set
|
18
|
+
@transition_table = transition_table
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :start, :accept_set, :transition_table
|
22
|
+
|
23
|
+
def accept?(state)
|
24
|
+
@accept_set.include? state
|
25
|
+
end
|
26
|
+
|
27
|
+
def destination(state, value)
|
28
|
+
@transition_table.destination(state, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "set"
|
2
|
+
require "resyma/core/utilities"
|
3
|
+
require "resyma/core/automaton/definition"
|
4
|
+
require "resyma/core/automaton/builder"
|
5
|
+
|
6
|
+
module Resyma
|
7
|
+
module Core
|
8
|
+
class EpsilonClass
|
9
|
+
include Matchable
|
10
|
+
end
|
11
|
+
|
12
|
+
Epsilon = EpsilonClass.new
|
13
|
+
|
14
|
+
class Automaton
|
15
|
+
#
|
16
|
+
# Computes the epsilon-closure of `state`
|
17
|
+
#
|
18
|
+
# @param [Set<Resyma::Core::State>] state_set Starting set of state
|
19
|
+
#
|
20
|
+
# @return [Set<Resyma::Core::State>] The epsilon-closure
|
21
|
+
#
|
22
|
+
def eclose(state_set)
|
23
|
+
queue = state_set.to_a
|
24
|
+
closure = queue.to_set
|
25
|
+
until queue.empty?
|
26
|
+
next_state = queue.shift
|
27
|
+
transition_table.candidates(next_state).each do |can|
|
28
|
+
next unless can.condition.equal?(Epsilon) &&
|
29
|
+
!closure.include?(can.destination)
|
30
|
+
|
31
|
+
closure.add(can.destination)
|
32
|
+
queue.push(can.destination)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
closure
|
36
|
+
end
|
37
|
+
|
38
|
+
def possible_nonepsilon_conditions(state)
|
39
|
+
transition_table.candidates(state).map(&:condition).select do |cond|
|
40
|
+
!cond.equal?(Epsilon)
|
41
|
+
end.to_set
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Computes next epsilon-closures connecting with `epsilon_closure`
|
46
|
+
#
|
47
|
+
# @param [Set<Resyma::Core::State>] epsilon_closure Current closure
|
48
|
+
# @param [Hash] closure_map Hash map from closures to states, may be
|
49
|
+
# modified
|
50
|
+
# @param [Resyma::Core::AutomatonBuilder] ab Builder of the new DFA, may
|
51
|
+
# be modified
|
52
|
+
#
|
53
|
+
# @return [Array<Set>] New unrecorded epsilon-closures
|
54
|
+
#
|
55
|
+
def generate_reachable_epsilon_closures(epsilon_closure, closure_map, ab)
|
56
|
+
current_state = closure_map[epsilon_closure]
|
57
|
+
condition_sets = epsilon_closure.map do |state|
|
58
|
+
possible_nonepsilon_conditions(state)
|
59
|
+
end
|
60
|
+
new_closures = []
|
61
|
+
Utils.big_union(condition_sets).each do |cond|
|
62
|
+
new_closure = Set[]
|
63
|
+
epsilon_closure.each do |state|
|
64
|
+
# [WARN] We are using a `matchable` as a `matched value, which may
|
65
|
+
# cause unexpected consequence
|
66
|
+
dst = destination(state, cond)
|
67
|
+
new_closure.add dst unless dst.nil?
|
68
|
+
end
|
69
|
+
raise "Internal error: No destination states" if new_closure.empty?
|
70
|
+
|
71
|
+
new_closure = eclose(new_closure)
|
72
|
+
if closure_map.include?(new_closure)
|
73
|
+
recorded_state = closure_map[new_closure]
|
74
|
+
ab.add_transition!(current_state, cond, recorded_state)
|
75
|
+
else
|
76
|
+
new_state = ab.new_state!
|
77
|
+
closure_map[new_closure] = new_state
|
78
|
+
ab.add_transition!(current_state, cond, new_state)
|
79
|
+
new_closures.push new_closure
|
80
|
+
end
|
81
|
+
end
|
82
|
+
new_closures
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_DFA
|
86
|
+
ab = AutomatonBuilder.new
|
87
|
+
start_closure = eclose(Set[start])
|
88
|
+
start_state = ab.new_state!
|
89
|
+
ab.start! start_state
|
90
|
+
closure_map = { start_closure => start_state }
|
91
|
+
queue = [start_closure]
|
92
|
+
until queue.empty?
|
93
|
+
next_closure = queue.shift
|
94
|
+
unrecorded_closures = generate_reachable_epsilon_closures(
|
95
|
+
next_closure, closure_map, ab
|
96
|
+
)
|
97
|
+
queue += unrecorded_closures
|
98
|
+
end
|
99
|
+
closure_map.each do |closure, state|
|
100
|
+
ab.accept! state if closure.any? { |s| accept? s }
|
101
|
+
end
|
102
|
+
ab.build
|
103
|
+
end
|
104
|
+
|
105
|
+
def has_epsilon?
|
106
|
+
transition_table.table.values.each do |cans|
|
107
|
+
cans.each do |can|
|
108
|
+
return true if can.condition.equal?(Epsilon)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|