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
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: %i[spec]
@@ -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,5 @@
1
+ require "resyma/core/algorithm/tuple"
2
+ require "resyma/core/algorithm/matcher"
3
+ require "resyma/core/algorithm/engine"
4
+
5
+ __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
@@ -0,0 +1,16 @@
1
+ module Resyma
2
+ module Core
3
+ module Matchable
4
+ #
5
+ # Matches with an object
6
+ #
7
+ # @param [Object] _other Value to be matched
8
+ #
9
+ # @return [true,false] Result
10
+ #
11
+ def match_with_value?(_other)
12
+ false
13
+ end
14
+ end
15
+ end
16
+ end