asmodis-rlsm 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENCE +674 -0
- data/README +35 -0
- data/lib/dfa.rb +686 -0
- data/lib/mgen.rb +130 -0
- data/lib/monkey_patching.rb +109 -0
- data/lib/monoid.rb +533 -0
- data/lib/rlsm.rb +22 -0
- data/lib/rlsm_regexp.rb +584 -0
- data/spec/dfa_spec.rb +99 -0
- data/spec/monoid_spec.rb +270 -0
- data/spec/regexp_spec.rb +25 -0
- metadata +64 -0
data/README
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
R egular
|
2
|
+
L anguages and
|
3
|
+
S yntactic
|
4
|
+
M onoids
|
5
|
+
|
6
|
+
=Short Description
|
7
|
+
|
8
|
+
This is a ruby implementation of three concepts:
|
9
|
+
- Deterministic Finite Automata (DFA)
|
10
|
+
- Regular Expressions (in the sense of theoretical computer sience)
|
11
|
+
- Monoids (an algebraic construct)
|
12
|
+
|
13
|
+
It is possible to convert
|
14
|
+
Monoid -> DFA
|
15
|
+
DFA -> RegExp
|
16
|
+
RegExp -> DFA
|
17
|
+
DFA -> Monoid
|
18
|
+
|
19
|
+
|
20
|
+
=Why?
|
21
|
+
|
22
|
+
In formal language theory, a language is a subset of the free monoid Sigma*, where Sigma is a finite set (the alphabet). An algebraic approach to study the properties of formal languages is given by the definition of the syntactic Monoid of a language.
|
23
|
+
|
24
|
+
A major result in this area is, that a language is regular iff its syntatcic monoid is finite. It is also known, that a language with a finite aperiodic monoid as syntactic monoid is star free (i.e. the corresponding regexp can be written without the Kleene-star.)
|
25
|
+
|
26
|
+
This code is written as an attemp to simplify the further study of finite monoids.
|
27
|
+
|
28
|
+
|
29
|
+
=Author
|
30
|
+
asmodis <g.diemant@gmx.net>
|
31
|
+
|
32
|
+
|
33
|
+
=License
|
34
|
+
GPL v3
|
35
|
+
Copyright 2008 Gunther Diemant
|
data/lib/dfa.rb
ADDED
@@ -0,0 +1,686 @@
|
|
1
|
+
# Copyright 2008 Gunther Diemant
|
2
|
+
#
|
3
|
+
# This file is part of the RLSM module.
|
4
|
+
#
|
5
|
+
# Foobar is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RLSM is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with RLSM. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
|
19
|
+
require File.join(File.dirname(__FILE__), 'monkey_patching')
|
20
|
+
require File.join(File.dirname(__FILE__), 'monoid')
|
21
|
+
require File.join(File.dirname(__FILE__), 'rlsm_regexp')
|
22
|
+
|
23
|
+
module RLSM
|
24
|
+
class DFA
|
25
|
+
def initialize(alph, states, initial, finals, transitions)
|
26
|
+
@alphabet = alph.sort
|
27
|
+
|
28
|
+
#State identifiers should be unique and shouldn't be trap
|
29
|
+
if states.uniq != states or states.include? 'trap'
|
30
|
+
raise Exception, "Bad states. State names must be unique and not 'trap'"
|
31
|
+
end
|
32
|
+
|
33
|
+
#Create the states
|
34
|
+
@states = states.map do |state|
|
35
|
+
if state == initial
|
36
|
+
if finals.include? state
|
37
|
+
State.new(self, state.to_s, :initial => true, :final => true)
|
38
|
+
else
|
39
|
+
State.new(self, state.to_s, :initial => true)
|
40
|
+
end
|
41
|
+
elsif finals.include? state
|
42
|
+
State.new(self, state.to_s, :final => true)
|
43
|
+
else
|
44
|
+
State.new(self, state.to_s)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#Add the transitions and check for completness
|
49
|
+
@states.each do |state|
|
50
|
+
transitions.select { |c, s1, s2| s1.to_s == state.label }.each do |c, s1, s2|
|
51
|
+
state.add_transition c, _get_state(s2.to_s)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#Calculate the reachable states
|
56
|
+
@reachable_states = [initial_state]
|
57
|
+
changed = true
|
58
|
+
|
59
|
+
while changed
|
60
|
+
changed = false
|
61
|
+
(@states - @reachable_states).each do |state|
|
62
|
+
if @reachable_states.any? { |s| s.reachable_states.include? state }
|
63
|
+
@reachable_states << state
|
64
|
+
changed = true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#Bring the initial state to index 0
|
70
|
+
ii = @states.index(initial_state)
|
71
|
+
if ii != 0
|
72
|
+
@states[0], @states[ii] = @states[ii], @states[0]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :alphabet, :reachable_states
|
77
|
+
|
78
|
+
#Returns the initial state
|
79
|
+
def initial_state
|
80
|
+
@initial ||= @states.find { |state| state.initial? }.deep_copy
|
81
|
+
end
|
82
|
+
|
83
|
+
#Returns an array of all final states.
|
84
|
+
def final_states
|
85
|
+
@finals ||= @states.find_all { |state| state.final? }.deep_copy
|
86
|
+
end
|
87
|
+
|
88
|
+
#Returns an array of all states.
|
89
|
+
def states
|
90
|
+
@states.deep_copy
|
91
|
+
end
|
92
|
+
|
93
|
+
#The number of states
|
94
|
+
def num_states
|
95
|
+
@states.size
|
96
|
+
end
|
97
|
+
|
98
|
+
#The number of final states.
|
99
|
+
def num_finals
|
100
|
+
final_states.size
|
101
|
+
end
|
102
|
+
|
103
|
+
#Returns the state in which the DFA halts if started in state and given str. Returns nil if this string isn't parseable by the DFA started in state. If the state is omitted, default start state is the initial state (surprising, isn't it).
|
104
|
+
def process(str, state = nil)
|
105
|
+
s = state || initial_state
|
106
|
+
str.each_char do |char|
|
107
|
+
s = s.process(char)
|
108
|
+
return nil if s.nil?
|
109
|
+
end
|
110
|
+
|
111
|
+
s
|
112
|
+
end
|
113
|
+
|
114
|
+
#Returns true if the given string is accepted by the DFA.
|
115
|
+
def accepts?(str)
|
116
|
+
s = process(str)
|
117
|
+
#Full String parsed, are we now in a final state or was an error?
|
118
|
+
s ? s.final? : false
|
119
|
+
end
|
120
|
+
|
121
|
+
#Returns an array of states. The state at position i is the state in which the DFA halts, if started in states[i] and given str.
|
122
|
+
#
|
123
|
+
#Caution: If a state can't process str, for this state nil is returned. In a complete DFA for any valid input string, a non nil State will be returnd.
|
124
|
+
def transition_function(str)
|
125
|
+
@states.map { |state| process(str, state) }
|
126
|
+
end
|
127
|
+
|
128
|
+
#Returns true if a DFA isomorphism between self and other exists.
|
129
|
+
def isomorph_to?(other)
|
130
|
+
#First a few necessary conditions
|
131
|
+
return false if num_states != other.num_states
|
132
|
+
return false if num_finals != other.num_finals
|
133
|
+
return false if alphabet != other.alphabet
|
134
|
+
|
135
|
+
initial_index = @states.index(initial_state)
|
136
|
+
final_indices = final_states.map { |f| @states.index(f) }
|
137
|
+
(0...num_states).to_a.permutations.each do |per|
|
138
|
+
#Initial state must map to initial state
|
139
|
+
next unless other.states[per[initial_index]].initial?
|
140
|
+
|
141
|
+
#Finals must map to finals
|
142
|
+
next unless final_indices.all? { |fi| other.states[fi].final? }
|
143
|
+
|
144
|
+
#Transactions respected?
|
145
|
+
bad = @states.find do |state|
|
146
|
+
not @alphabet.all? do |char|
|
147
|
+
si = @states.index(state)
|
148
|
+
ps = state.process(char)
|
149
|
+
|
150
|
+
if ps.nil?
|
151
|
+
other.states[per[si]].process(char).nil?
|
152
|
+
else
|
153
|
+
os = other.states[per[si]].process(char)
|
154
|
+
if os.nil?
|
155
|
+
false
|
156
|
+
else
|
157
|
+
per[@states.index(ps)] == other.states.index(os)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
#Found an iso, i.e. no bad states?
|
164
|
+
return true unless bad
|
165
|
+
end
|
166
|
+
|
167
|
+
#No isomorphism found
|
168
|
+
return false
|
169
|
+
end
|
170
|
+
|
171
|
+
#A synonym for isomorph_to?
|
172
|
+
def ==(other)
|
173
|
+
isomorph_to? other
|
174
|
+
end
|
175
|
+
|
176
|
+
#Returns true, if the two DFAs accepts the same language, i.e. the mimimized DFAs are isomorph.
|
177
|
+
def equivalent_to?(other)
|
178
|
+
minimze == other.minimize
|
179
|
+
end
|
180
|
+
|
181
|
+
#Returns true, if the two DFAs accepts languages of the same structure, i.e. if the languages differs only in the used alphabet. For example the languages aab and bba are similar.
|
182
|
+
def similar_to?(other)
|
183
|
+
dfa1 = minimize
|
184
|
+
dfa2 = other.minimize
|
185
|
+
|
186
|
+
#First a few necessary conditions
|
187
|
+
return false if dfa1.num_states != dfa2.num_states
|
188
|
+
return false if dfa1.num_finals != dfa2.num_finals
|
189
|
+
|
190
|
+
dfa1.alphabet.permutations do |per|
|
191
|
+
#Get the states
|
192
|
+
states = other.states.map { |st| st.label }
|
193
|
+
|
194
|
+
#Get the initial
|
195
|
+
initial = other.initial_state.label
|
196
|
+
|
197
|
+
#Get the finals
|
198
|
+
finals = other.final_states.map { |fs| fs.label }
|
199
|
+
|
200
|
+
#Get the transitions
|
201
|
+
trans = []
|
202
|
+
other.states.each do |s|
|
203
|
+
s.transitions.each_pair { |c,os| trans << [c,s.label, os.label].dup }
|
204
|
+
end
|
205
|
+
|
206
|
+
#Alter the transitions wrt to the permutation
|
207
|
+
trans.map! do |c,s1,s2|
|
208
|
+
[per[other.alphabet.index(c)],s1,s2]
|
209
|
+
end
|
210
|
+
|
211
|
+
#Exisits now an isomorphism between self and the new dfa?
|
212
|
+
return true if self == new(@alphabet, states, initial, finals, trans)
|
213
|
+
end
|
214
|
+
|
215
|
+
#No similarity found
|
216
|
+
return false
|
217
|
+
end
|
218
|
+
|
219
|
+
#Returns a minimal DFA which accepts the same language (see minimize!)
|
220
|
+
def minimize(opts = {})
|
221
|
+
self.deep_copy.minimize!(opts)
|
222
|
+
end
|
223
|
+
|
224
|
+
#Alters the DFA to a minimal one which accepts the same language.
|
225
|
+
#If the DFA is complete, then the minimal DFA returned is also complete, i.e. if there is a trap state (a dead state, but without, the DFA isn't complete), it will not be deleted. To do so, call remove_dead_states after minimizing.
|
226
|
+
#If passed :rename_states => true, the state labels will be renamed to something short (propably 0,1,2,...).
|
227
|
+
def minimize!(opts = {})
|
228
|
+
#First step: remove unreachable states
|
229
|
+
remove_unreachable_states!
|
230
|
+
|
231
|
+
complete_temp = complete?
|
232
|
+
|
233
|
+
#Second step: Find all equivalent states
|
234
|
+
#Create the initial state partition
|
235
|
+
sp = @states.partition { |state| state.final? }
|
236
|
+
sp_labels = sp.map { |sc| sc.map {|st| st.label} }.sort
|
237
|
+
|
238
|
+
#Calculate the new state partition for the first time
|
239
|
+
nsp = new_state_partition(sp)
|
240
|
+
nsp_labels = nsp.map { |sc| sc.map {|st| st.label} }.sort
|
241
|
+
|
242
|
+
#Find all state classes (repeating process until nothing changes)
|
243
|
+
while sp_labels != nsp_labels
|
244
|
+
sp, nsp = nsp.deep_copy, new_state_partition(nsp)
|
245
|
+
|
246
|
+
sp_labels = sp.map { |sc| sc.map {|st| st.label} }.sort
|
247
|
+
nsp_labels = nsp.map { |sc| sc.map {|st| st.label} }.sort
|
248
|
+
end
|
249
|
+
|
250
|
+
#Third step: Are we done?
|
251
|
+
#Check if the DFA was already minimal
|
252
|
+
return self if sp.all? { |sc| sc.size == 1 }
|
253
|
+
|
254
|
+
#Fourth step: Constructing the new DFA:
|
255
|
+
#1 the states
|
256
|
+
@states = sp.map do |sc|
|
257
|
+
state_label = sc.map { |s| s.label }.join
|
258
|
+
if sc.include? initial_state
|
259
|
+
if final_states.any? {|f| sc.include? f }
|
260
|
+
State.new(self, state_label, :initial => true, :final => true)
|
261
|
+
else
|
262
|
+
State.new(self, state_label, :initial => true)
|
263
|
+
end
|
264
|
+
elsif final_states.any? { |f| sc.include? f }
|
265
|
+
State.new(self, state_label, :final => true)
|
266
|
+
else
|
267
|
+
State.new(self, state_label)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
#2 the transitions
|
272
|
+
@states.each_with_index do |state, sc_index|
|
273
|
+
sp[sc_index].first.transitions.each_pair do |char, s2|
|
274
|
+
state.add_transition char, @states[_class_index_of(sp,s2)]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
#3 delete dead states
|
280
|
+
remove_dead_states!(:except_trap => complete_temp)
|
281
|
+
|
282
|
+
#4 Force recalculation of initial and final states
|
283
|
+
@initial, @finals = nil, nil
|
284
|
+
|
285
|
+
#Bring the initial state to index 0
|
286
|
+
ii = @states.index(initial_state)
|
287
|
+
if ii != 0
|
288
|
+
@states[0], @states[ii] = @states[ii], @states[0]
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
#5 Was renaming of the states requested?
|
293
|
+
if opts[:rename_states]
|
294
|
+
@states.each_with_index do |state,index|
|
295
|
+
state.label = index.to_s
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
#6 update the reachable states (all are reachable in a minimal DFA)
|
300
|
+
@reachable_states = @states.deep_copy
|
301
|
+
|
302
|
+
|
303
|
+
self
|
304
|
+
end
|
305
|
+
|
306
|
+
#Returns true if the DFA is minimal (see minimize!).
|
307
|
+
def minimal?
|
308
|
+
num_states == minimize.num_states
|
309
|
+
end
|
310
|
+
|
311
|
+
#Returns a complete DFA which accepts the same language.
|
312
|
+
def complete
|
313
|
+
self.deep_copy.complete!
|
314
|
+
end
|
315
|
+
|
316
|
+
#Adds a dead state and adds to every other state a transition to this state for all missing alphabet elements. If the DFA is already complete nothing happens.
|
317
|
+
#
|
318
|
+
#In either case the complete DFA is returned.
|
319
|
+
def complete!
|
320
|
+
#Is work to do?
|
321
|
+
return self if complete?
|
322
|
+
|
323
|
+
#Create the trap state
|
324
|
+
trap = State.new(self, 'trap')
|
325
|
+
@alphabet.each do |char|
|
326
|
+
trap.add_transition char, trap
|
327
|
+
end
|
328
|
+
|
329
|
+
#Add the necassery transitions
|
330
|
+
@states.each do |state|
|
331
|
+
unless state.complete?
|
332
|
+
(@alphabet - state.accepted_chars).each do |char|
|
333
|
+
state.add_transition char, trap
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
#Add the trap state to the DFA
|
339
|
+
@states << trap
|
340
|
+
@reachable_states << trap
|
341
|
+
|
342
|
+
self
|
343
|
+
end
|
344
|
+
|
345
|
+
#Returns true if this DFA is complete, i.e. all states accepts all alphabet symbols.
|
346
|
+
def complete?
|
347
|
+
@states.all? do |state|
|
348
|
+
state.complete?
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
#Returns an array of dead states (a state is dead, if it is not final and has outdegree 0)
|
353
|
+
def dead_states
|
354
|
+
@states.find_all { |state| state.dead? }
|
355
|
+
end
|
356
|
+
|
357
|
+
#Returns true if the DFA has dead states (see also dead_states)
|
358
|
+
def dead_states?
|
359
|
+
@states.find { |state| state.dead? } ? true : false
|
360
|
+
end
|
361
|
+
|
362
|
+
#Removes all dead_states. If passed :except_trap => true, trap states wouldn't be removed.
|
363
|
+
def remove_dead_states!(opt = {})
|
364
|
+
@states.each do |state|
|
365
|
+
state.remove_transitions_to_dead_states(opt)
|
366
|
+
end
|
367
|
+
|
368
|
+
#Remove the states
|
369
|
+
if opt[:except_trap]
|
370
|
+
@states = @states.reject do |state|
|
371
|
+
state.dead? and not state.trap?
|
372
|
+
end
|
373
|
+
@reachable_states = @reachable_states.reject do |state|
|
374
|
+
state.dead? and not state.trap?
|
375
|
+
end
|
376
|
+
else
|
377
|
+
@states = @states.reject { |state| state.dead? }
|
378
|
+
@reachable_states = @reachable_states.reject { |state| state.dead? }
|
379
|
+
end
|
380
|
+
|
381
|
+
self
|
382
|
+
end
|
383
|
+
|
384
|
+
#Returns a copy with dead states removed.
|
385
|
+
def remove_dead_states(opt = {})
|
386
|
+
self.deep_copy.remove_dead_states!(opt)
|
387
|
+
end
|
388
|
+
|
389
|
+
#Returns an array of states, wich aren't reachable from the initial state.
|
390
|
+
def unreachable_states
|
391
|
+
@states.find_all { |state| state.unreachable?}
|
392
|
+
end
|
393
|
+
|
394
|
+
#Returns true if the DFA has unreachable states
|
395
|
+
def unreachable_states?
|
396
|
+
@states.find { |state| state.unreachable? } ? true : false
|
397
|
+
end
|
398
|
+
|
399
|
+
#Removes all unreachable states.
|
400
|
+
def remove_unreachable_states!
|
401
|
+
#No transition update necessary, because each state which reaches an unreachble state must be unreachable.
|
402
|
+
@states = @states.reject { |state| state.unreachable? }
|
403
|
+
@reachable_states = @states.deep_copy
|
404
|
+
|
405
|
+
self
|
406
|
+
end
|
407
|
+
|
408
|
+
#Returns a copy with unreachable states removed.
|
409
|
+
def remove_unreachable_states
|
410
|
+
self.deep_copy.remove_unreachable_states!
|
411
|
+
end
|
412
|
+
|
413
|
+
#Returns an RLSM::RegExp instance representing the same language as this DFA.
|
414
|
+
def to_regexp
|
415
|
+
#No finals => no language
|
416
|
+
return RLSM::RegExp.new if final_states.empty?
|
417
|
+
|
418
|
+
#Calculate the coeffizients matrix
|
419
|
+
#1 empty matrix with lambdas for final states
|
420
|
+
matr = @states.map do |st|
|
421
|
+
last = st.final? ? [RLSM::RegExp.new('&')] : [RLSM::RegExp.new]
|
422
|
+
[RLSM::RegExp.new]*num_states + last
|
423
|
+
end
|
424
|
+
|
425
|
+
#2 remaining coeffizients
|
426
|
+
@states.each_with_index do |state,i|
|
427
|
+
state.transitions.each_pair do |ch, st|
|
428
|
+
matr[i][@states.index(st)] += RLSM::RegExp.new(ch)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
#Solve the matrix for matr[0][0] (Remember 0 is index of initial state)
|
433
|
+
(num_states-1).downto 1 do |i|
|
434
|
+
#Depends Ri on itself? If so apply well known simplify rule
|
435
|
+
# R = AR +B -> R = A*B
|
436
|
+
unless matr[i][i].empty?
|
437
|
+
matr[i].map! { |re| matr[i][i].star * re }
|
438
|
+
matr[i][i] = RLSM::RegExp.new
|
439
|
+
end
|
440
|
+
|
441
|
+
#Substitute know Ri in everey row above
|
442
|
+
ri = matr.pop
|
443
|
+
|
444
|
+
matr.map! do |row|
|
445
|
+
row.each_with_index do |re,j|
|
446
|
+
row[j] = re + ri[j]
|
447
|
+
end
|
448
|
+
|
449
|
+
row[i] = RLSM::RegExp.new
|
450
|
+
|
451
|
+
row
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
#Examine now the last remaining first row (irritating...)
|
456
|
+
regexp = matr.pop
|
457
|
+
|
458
|
+
if regexp[0].empty? #R0 depends not on R0
|
459
|
+
return regexp.last
|
460
|
+
else #R0 depends on R0
|
461
|
+
return regexp[0].star * regexp.last
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
#Calculate the transition monoid of the DFA. Because it is only possible for an complete DFA to compute the TM, the TM is calculated for the DFA returned by complete.
|
466
|
+
def transition_monoid
|
467
|
+
dfa = self.complete
|
468
|
+
|
469
|
+
#Calculate the monoid elements
|
470
|
+
trans_tab = [["", dfa.transition_function("")]]
|
471
|
+
|
472
|
+
new_elements = true
|
473
|
+
str_length = 1
|
474
|
+
while new_elements
|
475
|
+
new_elements = false
|
476
|
+
dfa.each_str_with_length(str_length) do |str|
|
477
|
+
tf = dfa.transition_function(str)
|
478
|
+
unless trans_tab.map { |s,f| f}.include? tf
|
479
|
+
trans_tab << [str, tf]
|
480
|
+
new_elements = true
|
481
|
+
end
|
482
|
+
end
|
483
|
+
str_length += 1
|
484
|
+
end
|
485
|
+
|
486
|
+
#Calculate the binary operation
|
487
|
+
binop = [(0...trans_tab.size).to_a]
|
488
|
+
|
489
|
+
(1...trans_tab.size).each do |i|
|
490
|
+
str = trans_tab[i].first
|
491
|
+
binop << trans_tab.map do |s, tf|
|
492
|
+
trans_tab.map {|st,f| f }.index(dfa.transition_function(str + s))
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
RLSM::Monoid.new binop.map { |row| row.join(',') }.join(' ')
|
498
|
+
end
|
499
|
+
|
500
|
+
#Returns the syntactic monoid which belongs to the language accepted by the DFA. (In fact, the transition monoid of the minimal DFA is returned, both monoids are isomorph)
|
501
|
+
def syntactic_monoid
|
502
|
+
minimize.transition_monoid
|
503
|
+
end
|
504
|
+
|
505
|
+
#Returns a string represantation
|
506
|
+
def to_s
|
507
|
+
@states.map { |state| state.to_s }.join("\n")
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
protected
|
512
|
+
def each_str_with_length(length)
|
513
|
+
if length == 0
|
514
|
+
yield ""
|
515
|
+
return
|
516
|
+
end
|
517
|
+
|
518
|
+
str = [@alphabet.first.clone]*length
|
519
|
+
pos = length-1
|
520
|
+
finished = false
|
521
|
+
loop do
|
522
|
+
yield str.join
|
523
|
+
|
524
|
+
loop do
|
525
|
+
if str[pos] == @alphabet.last
|
526
|
+
if pos == 0
|
527
|
+
finished = true
|
528
|
+
break
|
529
|
+
end
|
530
|
+
str[pos] = @alphabet.first
|
531
|
+
pos -= 1
|
532
|
+
else
|
533
|
+
str[pos] = @alphabet[@alphabet.index(str[pos])+1]
|
534
|
+
pos += 1 unless pos == length-1
|
535
|
+
break
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
break if finished
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
private
|
544
|
+
def _get_state(label)
|
545
|
+
@states.find { |state| state.label == label }
|
546
|
+
end
|
547
|
+
|
548
|
+
def _class_index_of(sp, s)
|
549
|
+
sp.each_with_index { |sc,i| return i if sc.include? s }
|
550
|
+
end
|
551
|
+
|
552
|
+
def new_state_partition(sp)
|
553
|
+
res = []
|
554
|
+
sp.each do |set|
|
555
|
+
partitionate_set(set, sp).compact.each do |s|
|
556
|
+
res << s
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
res
|
561
|
+
end
|
562
|
+
|
563
|
+
def partitionate_set(s, p)
|
564
|
+
#A singelton set is already partitionated
|
565
|
+
return s if s.empty?
|
566
|
+
|
567
|
+
acc_chars = s.first.accepted_chars.sort
|
568
|
+
|
569
|
+
#Find the classes in which the first element of s is transitionated
|
570
|
+
classes = {}
|
571
|
+
|
572
|
+
acc_chars.each do |c|
|
573
|
+
classes[c] = p.find { |set| set.include? s.first.process(c) }
|
574
|
+
end
|
575
|
+
|
576
|
+
first_class = s.find_all do |x|
|
577
|
+
acc_chars.all? { |c| classes[c].include? x.process(c) } and
|
578
|
+
acc_chars == x.accepted_chars.sort
|
579
|
+
end
|
580
|
+
|
581
|
+
rest = s - first_class
|
582
|
+
|
583
|
+
[first_class, *partitionate_set(rest, p)]
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
class DFA::State
|
588
|
+
def initialize(dfa, label, opts = {})
|
589
|
+
@label = label
|
590
|
+
@dfa = dfa
|
591
|
+
@is_initial = opts[:initial] || false
|
592
|
+
@is_final = opts[:final] || false
|
593
|
+
|
594
|
+
@trans = {}
|
595
|
+
end
|
596
|
+
|
597
|
+
attr_accessor :label
|
598
|
+
|
599
|
+
def initial?
|
600
|
+
@is_initial
|
601
|
+
end
|
602
|
+
|
603
|
+
def final?
|
604
|
+
@is_final
|
605
|
+
end
|
606
|
+
|
607
|
+
#Return true if this state is dead. A state is dead if there is know edge leading to another state and the state himself isn't a final state.
|
608
|
+
def dead?
|
609
|
+
(@trans.empty? or reachable_states == [self]) and not final?
|
610
|
+
end
|
611
|
+
|
612
|
+
#Return true if this state is a trap state. A trap state is a dead_state which accepts all alphabet elements and is reachable. A trap state is useful if a complete DFA is requested.
|
613
|
+
def trap?
|
614
|
+
dead? and reachable? and accepted_chars.sort == @dfa.alphabet.sort
|
615
|
+
end
|
616
|
+
|
617
|
+
def add_transition(char, dest_state)
|
618
|
+
if @trans.key? char
|
619
|
+
raise Exception, "Have already a transition for #{char}"
|
620
|
+
end
|
621
|
+
|
622
|
+
@trans[char] = dest_state
|
623
|
+
end
|
624
|
+
|
625
|
+
def process(char)
|
626
|
+
if @trans.key? char
|
627
|
+
@trans[char]
|
628
|
+
else
|
629
|
+
nil
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def complete?
|
634
|
+
@trans.size == @dfa.alphabet.size
|
635
|
+
end
|
636
|
+
|
637
|
+
def accepted_chars
|
638
|
+
@trans.keys
|
639
|
+
end
|
640
|
+
|
641
|
+
def reachable_states
|
642
|
+
@trans.values
|
643
|
+
end
|
644
|
+
|
645
|
+
def transitions
|
646
|
+
@trans
|
647
|
+
end
|
648
|
+
|
649
|
+
def remove_transitions_to_dead_states(opt = {})
|
650
|
+
if opt[:except_trap]
|
651
|
+
@trans = @trans.reject do |char, state|
|
652
|
+
state.dead? and not state.trap?
|
653
|
+
end
|
654
|
+
else
|
655
|
+
@trans = @trans.reject { |char, state| state.dead? }
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
def reachable?
|
660
|
+
@dfa.reachable_states.include? self
|
661
|
+
end
|
662
|
+
|
663
|
+
def unreachable?
|
664
|
+
not reachable?
|
665
|
+
end
|
666
|
+
|
667
|
+
def to_s
|
668
|
+
str = ''
|
669
|
+
str += '-> ' if initial?
|
670
|
+
str += '* ' if final?
|
671
|
+
str += @label.to_s + ': '
|
672
|
+
str += @trans.to_a.map { |c,s| c+' -> '+s.label.to_s }.join('; ')
|
673
|
+
|
674
|
+
str
|
675
|
+
end
|
676
|
+
|
677
|
+
def ==(other)
|
678
|
+
return false if other.class != DFA::State
|
679
|
+
return false if label != other.label
|
680
|
+
|
681
|
+
@trans.all? do |char, state|
|
682
|
+
state.label == other.process(char).label
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|