stamina-core 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +8 -0
- data/lib/stamina-core/stamina/automaton.rb +3 -356
- data/lib/stamina-core/stamina/automaton/edge.rb +79 -0
- data/lib/stamina-core/stamina/automaton/equivalence.rb +24 -43
- data/lib/stamina-core/stamina/automaton/metrics.rb +7 -13
- data/lib/stamina-core/stamina/automaton/minimize/pitchies.rb +3 -3
- data/lib/stamina-core/stamina/automaton/state.rb +267 -0
- data/lib/stamina-core/stamina/automaton/walking.rb +5 -5
- data/lib/stamina-core/stamina/core.rb +2 -1
- data/lib/stamina-core/stamina/markable.rb +14 -1
- data/lib/stamina-core/stamina/transition_system.rb +1 -0
- data/lib/stamina-core/stamina/transition_system/equivalence.rb +107 -0
- data/lib/stamina-core/stamina/utils/decorate.rb +123 -27
- data/lib/stamina-core/stamina/version.rb +2 -2
- metadata +12 -6
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.6.0 / 2012-09-12
|
2
|
+
|
3
|
+
* Added Markable#marks and Markable#raw_data
|
4
|
+
* State#initial!, #accepting! and error! now accept an optional truth value parameter.
|
5
|
+
* Generalized the decoration algorithm to work backwards as well as forwards as well as
|
6
|
+
accepting an output Hash-like object, defaulting to true state decorations.
|
7
|
+
* Generalized the DFA equivalence algorithm under TransitionSystem::Equivalence.
|
8
|
+
|
1
9
|
# O.5.4 / 2012-03-06
|
2
10
|
|
3
11
|
* InputString and Sample have been moved from stamina-induction to stamina-core as ADL
|
@@ -1,5 +1,6 @@
|
|
1
|
+
require_relative 'automaton/state'
|
2
|
+
require_relative 'automaton/edge'
|
1
3
|
module Stamina
|
2
|
-
|
3
4
|
#
|
4
5
|
# Automaton data-structure.
|
5
6
|
#
|
@@ -133,358 +134,6 @@ module Stamina
|
|
133
134
|
class Automaton
|
134
135
|
include Stamina::Markable
|
135
136
|
|
136
|
-
#
|
137
|
-
# Automaton state.
|
138
|
-
#
|
139
|
-
class State
|
140
|
-
include Stamina::Markable
|
141
|
-
attr_reader :automaton, :index
|
142
|
-
|
143
|
-
#
|
144
|
-
# Creates a state.
|
145
|
-
#
|
146
|
-
# Arguments:
|
147
|
-
# - automaton: parent automaton of the state.
|
148
|
-
# - index: index of the state in the state list.
|
149
|
-
# - data: user data attached to this state.
|
150
|
-
#
|
151
|
-
def initialize(automaton, index, data)
|
152
|
-
@automaton = automaton
|
153
|
-
@index = index
|
154
|
-
@data = data.dup
|
155
|
-
@out_edges = []
|
156
|
-
@in_edges = []
|
157
|
-
@epsilon_closure = nil
|
158
|
-
end
|
159
|
-
|
160
|
-
### public read-only section ###############################################
|
161
|
-
public
|
162
|
-
|
163
|
-
# Returns true if this state is an initial state, false otherwise.
|
164
|
-
def initial?
|
165
|
-
!!@data[:initial]
|
166
|
-
end
|
167
|
-
|
168
|
-
# Sets this state as an initial state.
|
169
|
-
def initial!
|
170
|
-
@data[:initial] = true
|
171
|
-
end
|
172
|
-
|
173
|
-
# Returns true if this state is an accepting state, false otherwise.
|
174
|
-
def accepting?
|
175
|
-
!!@data[:accepting]
|
176
|
-
end
|
177
|
-
|
178
|
-
# Sets this state as an accepting state.
|
179
|
-
def accepting!
|
180
|
-
@data[:accepting] = true
|
181
|
-
end
|
182
|
-
|
183
|
-
# Returns true if this state is an error state, false otherwise.
|
184
|
-
def error?
|
185
|
-
!!@data[:error]
|
186
|
-
end
|
187
|
-
|
188
|
-
# Sets this state as an error state.
|
189
|
-
def error!
|
190
|
-
@data[:error] = true
|
191
|
-
end
|
192
|
-
|
193
|
-
# Returns true if this state is deterministic, false otherwise.
|
194
|
-
def deterministic?
|
195
|
-
outs = out_symbols
|
196
|
-
(outs.size==@out_edges.size) and not(outs.include?(nil))
|
197
|
-
end
|
198
|
-
|
199
|
-
# Checks if this state is a sink state or not. Sink states are defined as
|
200
|
-
# non accepting states having no outgoing transition or only loop
|
201
|
-
# transitions.
|
202
|
-
def sink?
|
203
|
-
!accepting? && out_edges.all?{|e| e.target==self}
|
204
|
-
end
|
205
|
-
|
206
|
-
# Returns an array containing all incoming edges of the state. Edges are
|
207
|
-
# sorted if _sorted_ is set to true. If two incoming edges have same symbol
|
208
|
-
# no order is guaranteed between them.
|
209
|
-
#
|
210
|
-
# Returned array may be modified.
|
211
|
-
def in_edges(sorted=false)
|
212
|
-
sorted ? @in_edges.sort : @in_edges.dup
|
213
|
-
end
|
214
|
-
|
215
|
-
# Returns an array containing all outgoing edges of the state. Edges are
|
216
|
-
# sorted if _sorted_ is set to true. If two outgoing edges have same symbol
|
217
|
-
# no order is guaranteed between them.
|
218
|
-
#
|
219
|
-
# Returned array may be modified.
|
220
|
-
def out_edges(sorted=false)
|
221
|
-
sorted ? @out_edges.sort : @out_edges.dup
|
222
|
-
end
|
223
|
-
|
224
|
-
# Returns an array with the different symbols appearing on incoming edges.
|
225
|
-
# Returned array does not contain duplicates. Symbols are sorted in the
|
226
|
-
# array if _sorted_ is set to true.
|
227
|
-
#
|
228
|
-
# Returned array may be modified.
|
229
|
-
def in_symbols(sorted=false)
|
230
|
-
symbols = @in_edges.collect{|e| e.symbol}.uniq
|
231
|
-
return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
|
232
|
-
end
|
233
|
-
|
234
|
-
# Returns an array with the different symbols appearing on outgoing edges.
|
235
|
-
# Returned array does not contain duplicates. Symbols are sorted in the
|
236
|
-
# array if _sorted_ is set to true.
|
237
|
-
#
|
238
|
-
# Returned array may be modified.
|
239
|
-
def out_symbols(sorted=false)
|
240
|
-
symbols = @out_edges.collect{|e| e.symbol}.uniq
|
241
|
-
return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
|
242
|
-
end
|
243
|
-
|
244
|
-
# Returns an array with adjacent states (in or out edge).
|
245
|
-
#
|
246
|
-
# Returned array may be modified.
|
247
|
-
def adjacent_states()
|
248
|
-
(in_adjacent_states+out_adjacent_states).uniq
|
249
|
-
end
|
250
|
-
|
251
|
-
# Returns an array with adjacent states along an incoming edge (without
|
252
|
-
# duplicates).
|
253
|
-
#
|
254
|
-
# Returned array may be modified.
|
255
|
-
def in_adjacent_states()
|
256
|
-
(@in_edges.collect {|e| e.source}).uniq
|
257
|
-
end
|
258
|
-
|
259
|
-
# Returns an array with adjacent states along an outgoing edge (whithout
|
260
|
-
# duplicates).
|
261
|
-
#
|
262
|
-
# Returned array may be modified.
|
263
|
-
def out_adjacent_states()
|
264
|
-
(@out_edges.collect {|e| e.target}).uniq
|
265
|
-
end
|
266
|
-
|
267
|
-
# Returns reachable states from this one with an input _symbol_. Returned
|
268
|
-
# array does not contain duplicates and may be modified. This method if not
|
269
|
-
# epsilon symbol aware.
|
270
|
-
def step(symbol)
|
271
|
-
@out_edges.select{|e| e.symbol==symbol}.collect{|e| e.target}
|
272
|
-
end
|
273
|
-
|
274
|
-
# Returns the state reached from this one with an input _symbol_, or nil if
|
275
|
-
# no such state. This method is not epsilon symbol aware. Moreover it is
|
276
|
-
# expected to be used on deterministic states only. If the state is not
|
277
|
-
# deterministic, the method returns one reachable state if such a state
|
278
|
-
# exists; which one is returned must be considered non deterministic.
|
279
|
-
def dfa_step(symbol)
|
280
|
-
edge = @out_edges.find{|e| e.symbol==symbol}
|
281
|
-
edge ? edge.target : nil
|
282
|
-
end
|
283
|
-
|
284
|
-
# Computes the epsilon closure of this state. Epsilon closure is the set of
|
285
|
-
# all states reached from this one with a <tt>eps*</tt> input (sequence of
|
286
|
-
# zero or more epsilon symbols). The current state is always contained in
|
287
|
-
# the epsilon closure. Returns an unsorted array without duplicates; this
|
288
|
-
# array may not be modified.
|
289
|
-
def epsilon_closure()
|
290
|
-
@epsilon_closure ||= compute_epsilon_closure(Set.new).to_a.freeze
|
291
|
-
end
|
292
|
-
|
293
|
-
# Internal implementation of epsilon_closure. _result_ is expected to be
|
294
|
-
# a Set instance, is modified and is the returned value.
|
295
|
-
def compute_epsilon_closure(result)
|
296
|
-
result << self
|
297
|
-
step(nil).each do |t|
|
298
|
-
t.compute_epsilon_closure(result) unless result.include?(t)
|
299
|
-
end
|
300
|
-
raise if result.nil?
|
301
|
-
return result
|
302
|
-
end
|
303
|
-
|
304
|
-
# Computes an array representing the set of states that can be reached from
|
305
|
-
# this state with a given input _symbol_. Returned array does not contain
|
306
|
-
# duplicates and may be modified. No particular ordering of states in the
|
307
|
-
# array is guaranteed.
|
308
|
-
#
|
309
|
-
# This method is epsilon symbol aware (represented with nil) on non
|
310
|
-
# deterministic automata, meaning that it actually computes the set of
|
311
|
-
# reachable states through strings respecting the <tt>eps* symbol eps*</tt>
|
312
|
-
# regular expression, where eps is the epsilon symbol.
|
313
|
-
def delta(symbol)
|
314
|
-
if automaton.deterministic?
|
315
|
-
target = dfa_delta(symbol)
|
316
|
-
target.nil? ? [] : [target]
|
317
|
-
else
|
318
|
-
# 1) first compute epsilon closure of self
|
319
|
-
at_epsilon = epsilon_closure
|
320
|
-
|
321
|
-
# 2) now, look where we can go from there
|
322
|
-
at_espilon_then_symbol = at_epsilon.collect do |s|
|
323
|
-
s.step(symbol)
|
324
|
-
end.flatten.uniq
|
325
|
-
|
326
|
-
# 3) look where we can go from there using epsilon
|
327
|
-
result = at_espilon_then_symbol.collect do |s|
|
328
|
-
s.epsilon_closure
|
329
|
-
end.flatten.uniq
|
330
|
-
|
331
|
-
# return result as an array
|
332
|
-
result
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
# Returns the target state that can be reached from this state with _symbol_
|
337
|
-
# input. Returns nil if no such state exists.
|
338
|
-
#
|
339
|
-
# This method is expected to be used on deterministic automata. Unlike delta,
|
340
|
-
# it returns a State instance (or nil), not an array of states. When used on
|
341
|
-
# non deterministic automata, it returns a state immediately reachable from
|
342
|
-
# this state with _symbol_ input, or nil if no such state exists. This
|
343
|
-
# method is not epsilon aware.
|
344
|
-
def dfa_delta(symbol)
|
345
|
-
return nil if symbol.nil?
|
346
|
-
edge = @out_edges.find{|e| e.symbol==symbol}
|
347
|
-
edge.nil? ? nil : edge.target
|
348
|
-
end
|
349
|
-
|
350
|
-
# Provides comparator of states, based on the index in the automaton state
|
351
|
-
# list. This method returns nil unless _o_ is a State from the same
|
352
|
-
# automaton than self.
|
353
|
-
def <=>(o)
|
354
|
-
return nil unless State===o
|
355
|
-
return nil unless automaton===o.automaton
|
356
|
-
return index <=> o.index
|
357
|
-
end
|
358
|
-
|
359
|
-
# Returns a string representation
|
360
|
-
def inspect
|
361
|
-
's' << @index.to_s
|
362
|
-
end
|
363
|
-
|
364
|
-
# Returns a string representation
|
365
|
-
def to_s
|
366
|
-
's' << @index.to_s
|
367
|
-
end
|
368
|
-
|
369
|
-
### protected write section ################################################
|
370
|
-
protected
|
371
|
-
|
372
|
-
# Changes the index of this state in the state list. This method is only
|
373
|
-
# expected to be used by the automaton itself.
|
374
|
-
def index=(i) @index=i end
|
375
|
-
|
376
|
-
#
|
377
|
-
# Fired by Loaded when a user data is changed. The message is forwarded to
|
378
|
-
# the automaton.
|
379
|
-
#
|
380
|
-
def state_changed(what, description)
|
381
|
-
@epsilon_closure = nil
|
382
|
-
@automaton.send(:state_changed, what, description)
|
383
|
-
end
|
384
|
-
|
385
|
-
# Adds an incoming edge to the state.
|
386
|
-
def add_incoming_edge(edge)
|
387
|
-
@epsilon_closure = nil
|
388
|
-
@in_edges << edge
|
389
|
-
end
|
390
|
-
|
391
|
-
# Adds an outgoing edge to the state.
|
392
|
-
def add_outgoing_edge(edge)
|
393
|
-
@epsilon_closure = nil
|
394
|
-
@out_edges << edge
|
395
|
-
end
|
396
|
-
|
397
|
-
# Adds an incoming edge to the state.
|
398
|
-
def drop_incoming_edge(edge)
|
399
|
-
@epsilon_closure = nil
|
400
|
-
@in_edges.delete(edge)
|
401
|
-
end
|
402
|
-
|
403
|
-
# Adds an outgoing edge to the state.
|
404
|
-
def drop_outgoing_edge(edge)
|
405
|
-
@epsilon_closure = nil
|
406
|
-
@out_edges.delete(edge)
|
407
|
-
end
|
408
|
-
|
409
|
-
protected :compute_epsilon_closure
|
410
|
-
end
|
411
|
-
|
412
|
-
#
|
413
|
-
# Automaton edge.
|
414
|
-
#
|
415
|
-
class Edge
|
416
|
-
include Stamina::Markable
|
417
|
-
attr_reader :automaton, :index, :from, :to
|
418
|
-
|
419
|
-
#
|
420
|
-
# Creates an edge.
|
421
|
-
#
|
422
|
-
# Arguments:
|
423
|
-
# - automaton: parent automaton of the edge.
|
424
|
-
# - index: index of the edge in the edge list.
|
425
|
-
# - data: user data attached to this edge.
|
426
|
-
# - from: source state of the edge.
|
427
|
-
# - to: target state of the edge.
|
428
|
-
#
|
429
|
-
def initialize(automaton, index, data, from, to)
|
430
|
-
@automaton, @index = automaton, index
|
431
|
-
@data = data
|
432
|
-
@from, @to = from, to
|
433
|
-
end
|
434
|
-
|
435
|
-
# Returns edge symbol.
|
436
|
-
def symbol()
|
437
|
-
@data[:symbol]
|
438
|
-
end
|
439
|
-
|
440
|
-
# Sets edge symbol.
|
441
|
-
def symbol=(symbol)
|
442
|
-
@data[:symbol] = symbol
|
443
|
-
end
|
444
|
-
|
445
|
-
alias :source :from
|
446
|
-
alias :target :to
|
447
|
-
|
448
|
-
#
|
449
|
-
# Provides comparator of edges, based on the index in the automaton edge
|
450
|
-
# list. This method returns nil unless _o_ is an Edge from the same
|
451
|
-
# automaton than self.
|
452
|
-
# Once again, this method has nothing to do with equality, it looks at an
|
453
|
-
# index and ID only.
|
454
|
-
#
|
455
|
-
def <=>(o)
|
456
|
-
return nil unless Edge===o
|
457
|
-
return nil unless automaton===o.automaton
|
458
|
-
return index <=> o.index
|
459
|
-
end
|
460
|
-
|
461
|
-
# Returns a string representation
|
462
|
-
def inspect
|
463
|
-
'e' << @index.to_s
|
464
|
-
end
|
465
|
-
|
466
|
-
# Returns a string representation
|
467
|
-
def to_s
|
468
|
-
'e' << @index.to_s
|
469
|
-
end
|
470
|
-
|
471
|
-
### protected write section ################################################
|
472
|
-
protected
|
473
|
-
|
474
|
-
# Changes the index of this edge in the edge list. This method is only
|
475
|
-
# expected to be used by the automaton itself.
|
476
|
-
def index=(i) @index=i end
|
477
|
-
|
478
|
-
#
|
479
|
-
# Fired by Loaded when a user data is changed. The message if forwarded to
|
480
|
-
# the automaton.
|
481
|
-
#
|
482
|
-
def state_changed(what, infos)
|
483
|
-
@automaton.send(:state_changed, what, infos)
|
484
|
-
end
|
485
|
-
|
486
|
-
end
|
487
|
-
|
488
137
|
### Automaton class ##########################################################
|
489
138
|
public
|
490
139
|
|
@@ -1114,9 +763,7 @@ module Stamina
|
|
1114
763
|
|
1115
764
|
# Returns true if the automaton is deterministic, false otherwise
|
1116
765
|
def deterministic?
|
1117
|
-
if @deterministic.nil?
|
1118
|
-
@deterministic = @states.all?{|s| s.deterministic?}
|
1119
|
-
end
|
766
|
+
@deterministic = @states.all?{|s| s.deterministic?} if @deterministic.nil?
|
1120
767
|
@deterministic
|
1121
768
|
end
|
1122
769
|
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
#
|
4
|
+
# Automaton edge.
|
5
|
+
#
|
6
|
+
class Edge
|
7
|
+
include Stamina::Markable
|
8
|
+
attr_reader :automaton, :index, :from, :to
|
9
|
+
|
10
|
+
#
|
11
|
+
# Creates an edge.
|
12
|
+
#
|
13
|
+
# Arguments:
|
14
|
+
# - automaton: parent automaton of the edge.
|
15
|
+
# - index: index of the edge in the edge list.
|
16
|
+
# - data: user data attached to this edge.
|
17
|
+
# - from: source state of the edge.
|
18
|
+
# - to: target state of the edge.
|
19
|
+
#
|
20
|
+
def initialize(automaton, index, data, from, to)
|
21
|
+
@automaton, @index = automaton, index
|
22
|
+
@data = data
|
23
|
+
@from, @to = from, to
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns edge symbol.
|
27
|
+
def symbol()
|
28
|
+
@data[:symbol]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets edge symbol.
|
32
|
+
def symbol=(symbol)
|
33
|
+
@data[:symbol] = symbol
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :source :from
|
37
|
+
alias :target :to
|
38
|
+
|
39
|
+
#
|
40
|
+
# Provides comparator of edges, based on the index in the automaton edge
|
41
|
+
# list. This method returns nil unless _o_ is an Edge from the same
|
42
|
+
# automaton than self.
|
43
|
+
# Once again, this method has nothing to do with equality, it looks at an
|
44
|
+
# index and ID only.
|
45
|
+
#
|
46
|
+
def <=>(o)
|
47
|
+
return nil unless Edge===o
|
48
|
+
return nil unless automaton===o.automaton
|
49
|
+
return index <=> o.index
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a string representation
|
53
|
+
def inspect
|
54
|
+
'e' << @index.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a string representation
|
58
|
+
def to_s
|
59
|
+
'e' << @index.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
### protected write section ################################################
|
63
|
+
protected
|
64
|
+
|
65
|
+
# Changes the index of this edge in the edge list. This method is only
|
66
|
+
# expected to be used by the automaton itself.
|
67
|
+
def index=(i) @index=i end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Fired by Loaded when a user data is changed. The message if forwarded to
|
71
|
+
# the automaton.
|
72
|
+
#
|
73
|
+
def state_changed(what, infos)
|
74
|
+
@automaton.send(:state_changed, what, infos)
|
75
|
+
end
|
76
|
+
|
77
|
+
end # class Edge
|
78
|
+
end # class Automaton
|
79
|
+
end # module Stamina
|
@@ -1,55 +1,36 @@
|
|
1
1
|
module Stamina
|
2
2
|
class Automaton
|
3
3
|
|
4
|
+
# Implements the equivalence relation commonly used on canonical DFAs
|
5
|
+
class Equivalence < TransitionSystem::Equivalence
|
6
|
+
|
7
|
+
def equivalent_systems?(s, t)
|
8
|
+
(s.state_count == t.state_count) &&
|
9
|
+
(s.edge_count == t.edge_count) &&
|
10
|
+
(s.alphabet == t.alphabet) &&
|
11
|
+
(s.data == t.data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def equivalent_states?(s, t)
|
15
|
+
(s.initial? == t.initial?) &&
|
16
|
+
(s.accepting? == t.accepting?) &&
|
17
|
+
(s.error? == t.error?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def equivalent_edges?(e, f)
|
21
|
+
e.symbol == f.symbol
|
22
|
+
end
|
23
|
+
|
24
|
+
end # class Equivalence
|
25
|
+
|
4
26
|
#
|
5
27
|
# Checks if this automaton is equivalent to another one.
|
6
28
|
#
|
7
29
|
# Automata must be both minimal and complete to guarantee that this method
|
8
30
|
# works.
|
9
31
|
#
|
10
|
-
def equivalent?(other
|
11
|
-
|
12
|
-
(s1.accepting? == s2.accepting?) &&
|
13
|
-
(s1.error? == s2.error?) &&
|
14
|
-
(s1.initial? == s2.initial?) }
|
15
|
-
|
16
|
-
# Both must already have basic attributes in common
|
17
|
-
return false unless state_count==other.state_count
|
18
|
-
return false unless edge_count==other.edge_count
|
19
|
-
return false unless alphabet==other.alphabet
|
20
|
-
return false unless equiv[initial_state, other.initial_state]
|
21
|
-
|
22
|
-
# We instantiate the decoration algorithm for checking equivalence on this
|
23
|
-
# automaton:
|
24
|
-
# * decoration is the index of the equivalent state in other automaton
|
25
|
-
# * d0 is thus 'other.initial_state.index'
|
26
|
-
# * suppremum is identity and fails when the equivalent state is not unique
|
27
|
-
# * propagation checks transition function delta
|
28
|
-
#
|
29
|
-
algo = Stamina::Utils::Decorate.new(key)
|
30
|
-
algo.set_suppremum do |d0, d1|
|
31
|
-
if (d0.nil? or d1.nil?)
|
32
|
-
(d0 || d1)
|
33
|
-
elsif d0==d1
|
34
|
-
d0
|
35
|
-
else
|
36
|
-
raise Stamina::Abord
|
37
|
-
end
|
38
|
-
end
|
39
|
-
algo.set_propagate do |d,e|
|
40
|
-
reached = other.ith_state(d).dfa_step(e.symbol)
|
41
|
-
raise Stamina::Abord if reached.nil?
|
42
|
-
raise Stamina::Abord unless equiv[e.target, reached]
|
43
|
-
reached.index
|
44
|
-
end
|
45
|
-
|
46
|
-
# Run the algorithm now
|
47
|
-
begin
|
48
|
-
algo.execute(self, nil, other.initial_state.index)
|
49
|
-
return true
|
50
|
-
rescue Stamina::Abord
|
51
|
-
return false
|
52
|
-
end
|
32
|
+
def equivalent?(other)
|
33
|
+
Equivalence.new.call(self, other)
|
53
34
|
end
|
54
35
|
alias :<=> :equivalent?
|
55
36
|
|
@@ -49,23 +49,17 @@ module Stamina
|
|
49
49
|
# each state as a mark under _key_, which defaults to :depth.
|
50
50
|
#
|
51
51
|
def depth(key = :depth)
|
52
|
-
algo = Stamina::Utils::Decorate.new
|
52
|
+
algo = Stamina::Utils::Decorate.new
|
53
53
|
algo.set_suppremum do |d0,d1|
|
54
|
-
#
|
55
|
-
|
56
|
-
if d0.nil?
|
57
|
-
d1
|
58
|
-
elsif d1.nil?
|
59
|
-
d0
|
60
|
-
else
|
61
|
-
(d0 <= d1 ? d0 : d1)
|
62
|
-
end
|
54
|
+
# Unreached state is MAX (i.e. nil is +INF); we look at the min depth for each state
|
55
|
+
(d0.nil? or d1.nil?) ? (d0 || d1) : (d0 <= d1 ? d0 : d1)
|
63
56
|
end
|
64
57
|
algo.set_propagate{|d,e| d+1 }
|
65
|
-
algo.
|
58
|
+
algo.set_initiator{|s| s.initial? ? 0 : nil }
|
59
|
+
algo.set_start_predicate{|s| s.initial? }
|
60
|
+
algo.call(self, key)
|
66
61
|
deepest = states.max do |s0,s1|
|
67
|
-
#
|
68
|
-
# so that -1 is taken when nil is encountered
|
62
|
+
# do not take unreachable states into account: -1 is taken if nil is encountered
|
69
63
|
(s0[:depth] || -1) <=> (s1[:depth] || -1)
|
70
64
|
end
|
71
65
|
deepest[:depth]
|
@@ -107,13 +107,13 @@ module Stamina
|
|
107
107
|
def main
|
108
108
|
alph, states = @automaton.alphabet, @automaton.states
|
109
109
|
old_nb_states = -1
|
110
|
-
partition = states.
|
110
|
+
partition = states.map{|s| s.accepting? ? 1 : 0}
|
111
111
|
until (nb_states = partition.uniq.size) == old_nb_states
|
112
112
|
old_nb_states = nb_states
|
113
113
|
alph.each do |symbol|
|
114
|
-
reached = states.
|
114
|
+
reached = states.map{|s| partition[s.dfa_step(symbol).index]}
|
115
115
|
rehash = Hash.new{|h,k| h[k] = h.size}
|
116
|
-
partition = partition.zip(reached).
|
116
|
+
partition = partition.zip(reached).map{|pair| rehash[pair]}
|
117
117
|
end
|
118
118
|
end
|
119
119
|
minimized_dfa(@automaton, nb_states, partition)
|
@@ -0,0 +1,267 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
#
|
4
|
+
# Automaton state.
|
5
|
+
#
|
6
|
+
class State
|
7
|
+
include Stamina::Markable
|
8
|
+
attr_reader :automaton, :index
|
9
|
+
|
10
|
+
#
|
11
|
+
# Creates a state.
|
12
|
+
#
|
13
|
+
# Arguments:
|
14
|
+
# - automaton: parent automaton of the state.
|
15
|
+
# - index: index of the state in the state list.
|
16
|
+
# - data: user data attached to this state.
|
17
|
+
#
|
18
|
+
def initialize(automaton, index, data)
|
19
|
+
@automaton = automaton
|
20
|
+
@index = index
|
21
|
+
@data = data.dup
|
22
|
+
@out_edges = []
|
23
|
+
@in_edges = []
|
24
|
+
@epsilon_closure = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
### public read-only section ###############################################
|
28
|
+
public
|
29
|
+
|
30
|
+
# Returns true if this state is an initial state, false otherwise.
|
31
|
+
def initial?
|
32
|
+
!!@data[:initial]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sets this state as an initial state.
|
36
|
+
def initial!(value = true)
|
37
|
+
@data[:initial] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if this state is an accepting state, false otherwise.
|
41
|
+
def accepting?
|
42
|
+
!!@data[:accepting]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets this state as an accepting state.
|
46
|
+
def accepting!(value = true)
|
47
|
+
@data[:accepting] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns true if this state is an error state, false otherwise.
|
51
|
+
def error?
|
52
|
+
!!@data[:error]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets this state as an error state.
|
56
|
+
def error!(value = true)
|
57
|
+
@data[:error] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if this state is deterministic, false otherwise.
|
61
|
+
def deterministic?
|
62
|
+
outs = out_symbols
|
63
|
+
(outs.size==@out_edges.size) and not(outs.include?(nil))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Checks if this state is a sink state or not. Sink states are defined as
|
67
|
+
# non accepting states having no outgoing transition or only loop
|
68
|
+
# transitions.
|
69
|
+
def sink?
|
70
|
+
!accepting? && out_edges.all?{|e| e.target==self}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns an array containing all incoming edges of the state. Edges are
|
74
|
+
# sorted if _sorted_ is set to true. If two incoming edges have same symbol
|
75
|
+
# no order is guaranteed between them.
|
76
|
+
#
|
77
|
+
# Returned array may be modified.
|
78
|
+
def in_edges(sorted=false)
|
79
|
+
sorted ? @in_edges.sort : @in_edges.dup
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns an array containing all outgoing edges of the state. Edges are
|
83
|
+
# sorted if _sorted_ is set to true. If two outgoing edges have same symbol
|
84
|
+
# no order is guaranteed between them.
|
85
|
+
#
|
86
|
+
# Returned array may be modified.
|
87
|
+
def out_edges(sorted=false)
|
88
|
+
sorted ? @out_edges.sort : @out_edges.dup
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an array with the different symbols appearing on incoming edges.
|
92
|
+
# Returned array does not contain duplicates. Symbols are sorted in the
|
93
|
+
# array if _sorted_ is set to true.
|
94
|
+
#
|
95
|
+
# Returned array may be modified.
|
96
|
+
def in_symbols(sorted=false)
|
97
|
+
symbols = @in_edges.collect{|e| e.symbol}.uniq
|
98
|
+
return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns an array with the different symbols appearing on outgoing edges.
|
102
|
+
# Returned array does not contain duplicates. Symbols are sorted in the
|
103
|
+
# array if _sorted_ is set to true.
|
104
|
+
#
|
105
|
+
# Returned array may be modified.
|
106
|
+
def out_symbols(sorted=false)
|
107
|
+
symbols = @out_edges.collect{|e| e.symbol}.uniq
|
108
|
+
return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns an array with adjacent states (in or out edge).
|
112
|
+
#
|
113
|
+
# Returned array may be modified.
|
114
|
+
def adjacent_states()
|
115
|
+
(in_adjacent_states+out_adjacent_states).uniq
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns an array with adjacent states along an incoming edge (without
|
119
|
+
# duplicates).
|
120
|
+
#
|
121
|
+
# Returned array may be modified.
|
122
|
+
def in_adjacent_states()
|
123
|
+
(@in_edges.collect {|e| e.source}).uniq
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns an array with adjacent states along an outgoing edge (whithout
|
127
|
+
# duplicates).
|
128
|
+
#
|
129
|
+
# Returned array may be modified.
|
130
|
+
def out_adjacent_states()
|
131
|
+
(@out_edges.collect {|e| e.target}).uniq
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns reachable states from this one with an input _symbol_. Returned
|
135
|
+
# array does not contain duplicates and may be modified. This method if not
|
136
|
+
# epsilon symbol aware.
|
137
|
+
def step(symbol)
|
138
|
+
@out_edges.select{|e| e.symbol==symbol}.collect{|e| e.target}
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns the state reached from this one with an input _symbol_, or nil if
|
142
|
+
# no such state. This method is not epsilon symbol aware. Moreover it is
|
143
|
+
# expected to be used on deterministic states only. If the state is not
|
144
|
+
# deterministic, the method returns one reachable state if such a state
|
145
|
+
# exists; which one is returned must be considered non deterministic.
|
146
|
+
def dfa_step(symbol)
|
147
|
+
edge = @out_edges.find{|e| e.symbol==symbol}
|
148
|
+
edge ? edge.target : nil
|
149
|
+
end
|
150
|
+
|
151
|
+
# Computes the epsilon closure of this state. Epsilon closure is the set of
|
152
|
+
# all states reached from this one with a <tt>eps*</tt> input (sequence of
|
153
|
+
# zero or more epsilon symbols). The current state is always contained in
|
154
|
+
# the epsilon closure. Returns an unsorted array without duplicates; this
|
155
|
+
# array may not be modified.
|
156
|
+
def epsilon_closure()
|
157
|
+
@epsilon_closure ||= compute_epsilon_closure(Set.new).to_a.freeze
|
158
|
+
end
|
159
|
+
|
160
|
+
# Internal implementation of epsilon_closure. _result_ is expected to be
|
161
|
+
# a Set instance, is modified and is the returned value.
|
162
|
+
def compute_epsilon_closure(result)
|
163
|
+
result << self
|
164
|
+
step(nil).each do |t|
|
165
|
+
t.compute_epsilon_closure(result) unless result.include?(t)
|
166
|
+
end
|
167
|
+
raise if result.nil?
|
168
|
+
return result
|
169
|
+
end
|
170
|
+
|
171
|
+
# Computes an array representing the set of states that can be reached from
|
172
|
+
# this state with a given input _symbol_. Returned array does not contain
|
173
|
+
# duplicates and may be modified. No particular ordering of states in the
|
174
|
+
# array is guaranteed.
|
175
|
+
#
|
176
|
+
# This method is epsilon symbol aware (represented with nil) on non
|
177
|
+
# deterministic automata, meaning that it actually computes the set of
|
178
|
+
# reachable states through strings respecting the <tt>eps* symbol eps*</tt>
|
179
|
+
# regular expression, where eps is the epsilon symbol.
|
180
|
+
def delta(symbol)
|
181
|
+
if automaton.deterministic?
|
182
|
+
target = dfa_delta(symbol)
|
183
|
+
target.nil? ? [] : [target]
|
184
|
+
else
|
185
|
+
at_epsilon = epsilon_closure
|
186
|
+
at_espilon_then_symbol = at_epsilon.map{|s| s.step(symbol)}.flatten.uniq
|
187
|
+
at_espilon_then_symbol.map{|s| s.epsilon_closure}.flatten.uniq
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns the target state that can be reached from this state with _symbol_
|
192
|
+
# input. Returns nil if no such state exists.
|
193
|
+
#
|
194
|
+
# This method is expected to be used on deterministic automata. Unlike delta,
|
195
|
+
# it returns a State instance (or nil), not an array of states. When used on
|
196
|
+
# non deterministic automata, it returns a state immediately reachable from
|
197
|
+
# this state with _symbol_ input, or nil if no such state exists. This
|
198
|
+
# method is not epsilon aware.
|
199
|
+
def dfa_delta(symbol)
|
200
|
+
return nil if symbol.nil?
|
201
|
+
edge = @out_edges.find{|e| e.symbol==symbol}
|
202
|
+
edge.nil? ? nil : edge.target
|
203
|
+
end
|
204
|
+
|
205
|
+
# Provides comparator of states, based on the index in the automaton state
|
206
|
+
# list. This method returns nil unless _o_ is a State from the same
|
207
|
+
# automaton than self.
|
208
|
+
def <=>(o)
|
209
|
+
return nil unless State===o
|
210
|
+
return nil unless automaton===o.automaton
|
211
|
+
return index <=> o.index
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns a string representation
|
215
|
+
def inspect
|
216
|
+
's' << @index.to_s
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns a string representation
|
220
|
+
def to_s
|
221
|
+
's' << @index.to_s
|
222
|
+
end
|
223
|
+
|
224
|
+
### protected write section ################################################
|
225
|
+
protected
|
226
|
+
|
227
|
+
# Changes the index of this state in the state list. This method is only
|
228
|
+
# expected to be used by the automaton itself.
|
229
|
+
def index=(i) @index=i end
|
230
|
+
|
231
|
+
#
|
232
|
+
# Fired by Loaded when a user data is changed. The message is forwarded to
|
233
|
+
# the automaton.
|
234
|
+
#
|
235
|
+
def state_changed(what, description)
|
236
|
+
@epsilon_closure = nil
|
237
|
+
@automaton.send(:state_changed, what, description)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Adds an incoming edge to the state.
|
241
|
+
def add_incoming_edge(edge)
|
242
|
+
@epsilon_closure = nil
|
243
|
+
@in_edges << edge
|
244
|
+
end
|
245
|
+
|
246
|
+
# Adds an outgoing edge to the state.
|
247
|
+
def add_outgoing_edge(edge)
|
248
|
+
@epsilon_closure = nil
|
249
|
+
@out_edges << edge
|
250
|
+
end
|
251
|
+
|
252
|
+
# Adds an incoming edge to the state.
|
253
|
+
def drop_incoming_edge(edge)
|
254
|
+
@epsilon_closure = nil
|
255
|
+
@in_edges.delete(edge)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Adds an outgoing edge to the state.
|
259
|
+
def drop_outgoing_edge(edge)
|
260
|
+
@epsilon_closure = nil
|
261
|
+
@out_edges.delete(edge)
|
262
|
+
end
|
263
|
+
|
264
|
+
protected :compute_epsilon_closure
|
265
|
+
end # class State
|
266
|
+
end # class Automaton
|
267
|
+
end # module Stamina
|
@@ -135,7 +135,7 @@ module Stamina
|
|
135
135
|
#
|
136
136
|
def step(from, symbol)
|
137
137
|
from = walking_to_from(from)
|
138
|
-
from.
|
138
|
+
from.map{|s| s.step(symbol)}.flatten.uniq
|
139
139
|
end
|
140
140
|
|
141
141
|
#
|
@@ -144,7 +144,7 @@ module Stamina
|
|
144
144
|
# reached with the given symbol.
|
145
145
|
#
|
146
146
|
def dfa_step(from, symbol)
|
147
|
-
step = walking_to_from(from).
|
147
|
+
step = walking_to_from(from).map{|s| s.dfa_step(symbol)}.flatten.uniq
|
148
148
|
walking_to_dfa_result(step, from)
|
149
149
|
end
|
150
150
|
|
@@ -158,7 +158,7 @@ module Stamina
|
|
158
158
|
# regular expression, where eps is the epsilon symbol.
|
159
159
|
#
|
160
160
|
def delta(from, symbol)
|
161
|
-
walking_to_from(from).
|
161
|
+
walking_to_from(from).map{|s| s.delta(symbol)}.flatten.uniq
|
162
162
|
end
|
163
163
|
|
164
164
|
#
|
@@ -171,7 +171,7 @@ module Stamina
|
|
171
171
|
if from.is_a?(Automaton::State)
|
172
172
|
from.dfa_delta(symbol)
|
173
173
|
else
|
174
|
-
delta = walking_to_from(from).
|
174
|
+
delta = walking_to_from(from).map{|s| s.dfa_delta(symbol)}.flatten.uniq
|
175
175
|
walking_to_dfa_result(delta, from)
|
176
176
|
end
|
177
177
|
end
|
@@ -344,7 +344,7 @@ module Stamina
|
|
344
344
|
# Implements _from_ conventions.
|
345
345
|
def walking_to_from(from)
|
346
346
|
return initial_states if from.nil?
|
347
|
-
Array===from ? from.
|
347
|
+
Array===from ? from.map{|s| to_state(s)} : [to_state(from)]
|
348
348
|
end
|
349
349
|
|
350
350
|
# Implements _return_ conventions of dfa_xxx methods.
|
@@ -2,9 +2,10 @@ require_relative 'version'
|
|
2
2
|
require_relative 'errors'
|
3
3
|
require_relative 'ext/math'
|
4
4
|
require_relative 'markable'
|
5
|
+
require_relative 'utils'
|
6
|
+
require_relative 'transition_system'
|
5
7
|
require_relative 'adl'
|
6
8
|
require_relative 'automaton'
|
7
|
-
require_relative 'utils'
|
8
9
|
require_relative 'input_string'
|
9
10
|
require_relative 'sample'
|
10
11
|
require_relative 'dsl'
|
@@ -14,7 +14,9 @@ module Stamina
|
|
14
14
|
#
|
15
15
|
# Returns user-value associated to _key_, nil if no such key in user-data.
|
16
16
|
#
|
17
|
-
def [](key)
|
17
|
+
def [](key)
|
18
|
+
@data[key]
|
19
|
+
end
|
18
20
|
|
19
21
|
#
|
20
22
|
# Associates _value_ to _key_ in user-data. Overrides previous value if
|
@@ -33,6 +35,17 @@ module Stamina
|
|
33
35
|
state_changed(:loaded_pair, [key,oldvalue,nil]) if self.respond_to? :state_changed
|
34
36
|
end
|
35
37
|
|
38
|
+
# Returns the values mapped to `keys` as an array
|
39
|
+
def marks(*keys)
|
40
|
+
raw_data.values_at(*keys)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns RAW data, that is without duplicating it. Returns result should not be
|
44
|
+
# changed.
|
45
|
+
def raw_data
|
46
|
+
@data ||= {}
|
47
|
+
end
|
48
|
+
|
36
49
|
# Extracts the copy of attributes which can subsequently be modified.
|
37
50
|
def data
|
38
51
|
@data.nil? ? {} : @data.dup
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'transition_system/equivalence'
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Stamina
|
2
|
+
class TransitionSystem
|
3
|
+
class Equivalence
|
4
|
+
|
5
|
+
# Returns true if `s` and `t` must be considered equivalent, false otherwise.
|
6
|
+
def equivalent_systems?(s, t)
|
7
|
+
(s.state_count == t.state_count) &&
|
8
|
+
(s.edge_count == t.edge_count) &&
|
9
|
+
(s.raw_data == t.raw_data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def equivalent_systems!(s, t)
|
13
|
+
fail "Non equivalent systems `#{s}` and `#{t}`" unless equivalent_systems?(s,t)
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns true if `s` and `t` must be considered equivalent, false otherwise.
|
18
|
+
def equivalent_states?(s, t)
|
19
|
+
s.raw_data == t.raw_data
|
20
|
+
end
|
21
|
+
|
22
|
+
def equivalent_states!(s, t)
|
23
|
+
fail "Non equivalent states `#{s}` and `#{t}`" unless equivalent_states?(s,t)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns true if `e` and `f` must be considered equivalent, false otherwise.
|
28
|
+
def equivalent_edges?(e, f)
|
29
|
+
e.raw_data == f.raw_data
|
30
|
+
end
|
31
|
+
|
32
|
+
def equivalent_edges!(s, t)
|
33
|
+
fail "Non equivalent edges `#{s}` and `#{t}`" unless equivalent_edges?(s,t)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Finds the edge counterpart of `operand_edge` as an outgoing edge of
|
38
|
+
# `reference_state`. The default implementation takes an edge that shares the same
|
39
|
+
# symbol as operand_edge.
|
40
|
+
def find_edge_counterpart(reference_state, operand_edge)
|
41
|
+
symbol = operand_edge.symbol
|
42
|
+
reference_state.out_edges.find{|e| e.symbol==symbol}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Computes equivalence pairs through decoration
|
46
|
+
class EquivThroughDeco < Utils::Decorate
|
47
|
+
|
48
|
+
def initialize(reference, algo)
|
49
|
+
@reference = reference
|
50
|
+
@algo = algo
|
51
|
+
end
|
52
|
+
attr_reader :reference, :algo
|
53
|
+
|
54
|
+
def suppremum(d0, d1)
|
55
|
+
return (d0 || d1) if (d0.nil? or d1.nil?)
|
56
|
+
unless d0==d1
|
57
|
+
algo.fail("Different states found through same path: #{d0} & #{d1}")
|
58
|
+
end
|
59
|
+
d0
|
60
|
+
end
|
61
|
+
|
62
|
+
def propagate(deco, edge)
|
63
|
+
symbol = edge.symbol
|
64
|
+
source = reference.ith_state(deco)
|
65
|
+
eq_edge = algo.find_edge_counterpart(source, edge)
|
66
|
+
algo.fail("No such transition `#{symbol}` from #{source}") unless eq_edge
|
67
|
+
algo.equivalent_edges!(edge, eq_edge)
|
68
|
+
algo.equivalent_states!(edge.target, eq_edge.target)
|
69
|
+
eq_edge.target.index
|
70
|
+
end
|
71
|
+
|
72
|
+
def init_deco(s)
|
73
|
+
s.initial? ? reference.initial_state.index : nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def take_at_start?(s)
|
77
|
+
s.initial?
|
78
|
+
end
|
79
|
+
|
80
|
+
end # EquivThroughDeco
|
81
|
+
|
82
|
+
# Executes the equivalence algorithm on two transition systems `ts1` and `ts2`.
|
83
|
+
# Returns true if they are considered equivalent, false otherwise.
|
84
|
+
def call(ts1, ts2, &explain)
|
85
|
+
@explain = explain
|
86
|
+
catch(:fail) do
|
87
|
+
equivalent_systems!(ts1, ts2)
|
88
|
+
i1, i2 = ts1.initial_state, ts2.initial_state
|
89
|
+
fail "No initial state on ts1" unless i1
|
90
|
+
fail "No initial state on ts2" unless i2
|
91
|
+
equivalent_states!(i1, i2)
|
92
|
+
EquivThroughDeco.new(ts2, self).call(ts1, {})
|
93
|
+
return true
|
94
|
+
end
|
95
|
+
return false
|
96
|
+
ensure
|
97
|
+
@explain = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def fail(message)
|
101
|
+
@explain.call(message) if @explain
|
102
|
+
throw :fail
|
103
|
+
end
|
104
|
+
|
105
|
+
end # class Equivalence
|
106
|
+
end # class TransitionSystem
|
107
|
+
end # module Stamina
|
@@ -1,22 +1,35 @@
|
|
1
1
|
module Stamina
|
2
2
|
module Utils
|
3
3
|
#
|
4
|
-
# Decorates states of an automaton by applying a propagation rule
|
5
|
-
#
|
4
|
+
# Decorates states of an automaton by applying a propagation rule until a fix point is
|
5
|
+
# reached.
|
6
6
|
#
|
7
7
|
class Decorate
|
8
8
|
|
9
|
-
# The key to use to maintain the decoration on states (:invariant
|
10
|
-
# is used by default)
|
11
|
-
attr_writer :decoration_key
|
12
|
-
|
13
9
|
# Creates a decoration algorithm instance
|
14
|
-
def initialize(
|
15
|
-
@
|
10
|
+
def initialize(output = :invariant)
|
11
|
+
@output = Decorate.state_output(output) if output
|
16
12
|
@suppremum = nil
|
17
13
|
@propagate = nil
|
14
|
+
@initiator = nil
|
15
|
+
@backward = false
|
16
|
+
@start_predicate = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Builds an output hash that keeps decoration in states
|
20
|
+
def self.state_output(decoration_key)
|
21
|
+
Object.new.extend Module.new{
|
22
|
+
define_method :[] do |state|
|
23
|
+
state[decoration_key]
|
24
|
+
end
|
25
|
+
define_method :[]= do |state,deco|
|
26
|
+
state[decoration_key] = deco
|
27
|
+
end
|
28
|
+
}
|
18
29
|
end
|
19
30
|
|
31
|
+
### CONFIGURATION ##################################################################
|
32
|
+
|
20
33
|
# Installs a suppremum function through a block.
|
21
34
|
def set_suppremum(&block)
|
22
35
|
raise ArgumentError, 'Suppremum expected through a block' if block.nil?
|
@@ -24,6 +37,11 @@ module Stamina
|
|
24
37
|
@suppremum = block
|
25
38
|
end
|
26
39
|
|
40
|
+
# Same as #set_suppremum, but with an explicit proc
|
41
|
+
def suppremum=(proc)
|
42
|
+
set_suppremum(&proc)
|
43
|
+
end
|
44
|
+
|
27
45
|
# Installs a propagate function through a block.
|
28
46
|
def set_propagate(&block)
|
29
47
|
raise ArgumentError, 'Propagate expected through a block' if block.nil?
|
@@ -31,6 +49,43 @@ module Stamina
|
|
31
49
|
@propagate = block
|
32
50
|
end
|
33
51
|
|
52
|
+
# Same as #set_propagate, but with an explicit proc
|
53
|
+
def propagate=(proc)
|
54
|
+
set_propagate(&proc)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set an initiator methods, responsible of computing the initial decoration of
|
58
|
+
# each state
|
59
|
+
def set_initiator(&block)
|
60
|
+
raise ArgumentError, 'Initiator expected through a block' if block.nil?
|
61
|
+
raise ArgumentError, 'Block of arity 1 expected' unless block.arity==1
|
62
|
+
@initiator = block
|
63
|
+
end
|
64
|
+
|
65
|
+
# Same as #set_initiator but with an explicit proc
|
66
|
+
def initiator=(proc)
|
67
|
+
set_initiator(&proc)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sets the start predicate to use
|
71
|
+
def set_start_predicate(&block)
|
72
|
+
raise ArgumentError, 'Start predicate expected through a block' if block.nil?
|
73
|
+
raise ArgumentError, 'Block of arity 1 expected' unless block.arity==1
|
74
|
+
@start_predicate = block
|
75
|
+
end
|
76
|
+
|
77
|
+
# Same as #set_start_predicate but with an explicit proc
|
78
|
+
def start_predicate=(proc)
|
79
|
+
set_start_predicate(&proc)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sets if the algorithms works backward
|
83
|
+
def backward=(val)
|
84
|
+
@backward = val
|
85
|
+
end
|
86
|
+
|
87
|
+
### SUBCLASS HOOKS #################################################################
|
88
|
+
|
34
89
|
# Computes the suppremum between two decorations. By default, this method
|
35
90
|
# looks for a suppremum function installed with set_suppremum. If not found,
|
36
91
|
# it tries calling a suppremum method on d0. If not found it raises an error.
|
@@ -51,31 +106,72 @@ module Stamina
|
|
51
106
|
raise "No propagate function installed or implemented by decorations"
|
52
107
|
end
|
53
108
|
|
54
|
-
#
|
55
|
-
def
|
56
|
-
|
109
|
+
# Returns the initial decoration of state `s`
|
110
|
+
def init_deco(s)
|
111
|
+
return @initiator.call(s) if @initiator
|
112
|
+
raise "No initiator function installed"
|
113
|
+
end
|
57
114
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
115
|
+
# Returns the start predicate
|
116
|
+
def take_at_start?(s)
|
117
|
+
return @start_predicate.call(s) if @start_predicate
|
118
|
+
raise "No start predicate function installed"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Work backward?
|
122
|
+
def backward?
|
123
|
+
@backward
|
124
|
+
end
|
63
125
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
126
|
+
### MAIN ###########################################################################
|
127
|
+
|
128
|
+
# Executes the propagation algorithm on a given automaton.
|
129
|
+
def call(fa, out = nil)
|
130
|
+
with_output(out) do |output|
|
131
|
+
fa.states.each{|s| output[s] = init_deco(s) } # Init decoration on each state
|
132
|
+
to_explore = fa.states.select{|s| take_at_start?(s)} # Init to_explore (start predicate)
|
133
|
+
until to_explore.empty? # empty to_explore now
|
134
|
+
source = to_explore.pop
|
135
|
+
each_edge_and_target(source) do |edge, target|
|
136
|
+
p_decor = propagate(output[source], edge)
|
137
|
+
p_decor = suppremum(output[target], p_decor)
|
138
|
+
unless p_decor == output[target]
|
139
|
+
output[target] = p_decor
|
140
|
+
to_explore << target unless to_explore.include?(target)
|
141
|
+
end
|
74
142
|
end
|
75
143
|
end
|
144
|
+
fa
|
76
145
|
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Executes the propagation algorithm on a given automaton.
|
149
|
+
def execute(fa, bottom, d0)
|
150
|
+
warn "Decorate#execute is deprecated, use Decorate#call (#{caller[0]})"
|
151
|
+
self.initiator = lambda{|s| (s.initial? ? d0 : bottom)}
|
152
|
+
self.start_predicate = lambda{|s| s.initial? }
|
153
|
+
call(fa)
|
154
|
+
end
|
77
155
|
|
78
|
-
|
156
|
+
private
|
157
|
+
|
158
|
+
def with_output(out)
|
159
|
+
if out
|
160
|
+
yield out.is_a?(Symbol) ? Decorate.state_output(out) : out
|
161
|
+
elsif @output
|
162
|
+
warn "Decorate.new(:decokey) is deprecated, use Decorate#call(fa, :decokey) (#{caller[1]})"
|
163
|
+
yield @output
|
164
|
+
else
|
165
|
+
raise ArgumentError, "Output may not be nil", caller
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def each_edge_and_target(source)
|
170
|
+
edges = backward? ? source.in_edges : source.out_edges
|
171
|
+
edges.each do |edge|
|
172
|
+
target = target = backward? ? edge.source : edge.target
|
173
|
+
yield edge, target
|
174
|
+
end
|
79
175
|
end
|
80
176
|
|
81
177
|
end # class Decorate
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stamina-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: quickl
|
16
|
-
requirement: &
|
16
|
+
requirement: &70185698728600 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: 0.4.3
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70185698728600
|
25
25
|
description: ! "Stamina is an automaton and regular inference toolkit initially developped
|
26
26
|
for the \nbaseline of the Stamina Competition (stamina.chefbe.net)."
|
27
27
|
email:
|
@@ -37,12 +37,14 @@ files:
|
|
37
37
|
- lib/stamina-core/stamina/automaton/complete.rb
|
38
38
|
- lib/stamina-core/stamina/automaton/compose.rb
|
39
39
|
- lib/stamina-core/stamina/automaton/determinize.rb
|
40
|
+
- lib/stamina-core/stamina/automaton/edge.rb
|
40
41
|
- lib/stamina-core/stamina/automaton/equivalence.rb
|
41
42
|
- lib/stamina-core/stamina/automaton/hide.rb
|
42
43
|
- lib/stamina-core/stamina/automaton/metrics.rb
|
43
44
|
- lib/stamina-core/stamina/automaton/minimize/hopcroft.rb
|
44
45
|
- lib/stamina-core/stamina/automaton/minimize/pitchies.rb
|
45
46
|
- lib/stamina-core/stamina/automaton/minimize.rb
|
47
|
+
- lib/stamina-core/stamina/automaton/state.rb
|
46
48
|
- lib/stamina-core/stamina/automaton/strip.rb
|
47
49
|
- lib/stamina-core/stamina/automaton/walking.rb
|
48
50
|
- lib/stamina-core/stamina/automaton.rb
|
@@ -62,6 +64,8 @@ files:
|
|
62
64
|
- lib/stamina-core/stamina/input_string.rb
|
63
65
|
- lib/stamina-core/stamina/markable.rb
|
64
66
|
- lib/stamina-core/stamina/sample.rb
|
67
|
+
- lib/stamina-core/stamina/transition_system/equivalence.rb
|
68
|
+
- lib/stamina-core/stamina/transition_system.rb
|
65
69
|
- lib/stamina-core/stamina/utils/decorate.rb
|
66
70
|
- lib/stamina-core/stamina/utils.rb
|
67
71
|
- lib/stamina-core/stamina/version.rb
|
@@ -78,6 +82,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
82
|
- - ! '>='
|
79
83
|
- !ruby/object:Gem::Version
|
80
84
|
version: '0'
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
hash: -57027985138010278
|
81
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
89
|
none: false
|
83
90
|
requirements:
|
@@ -86,9 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
93
|
version: '0'
|
87
94
|
requirements: []
|
88
95
|
rubyforge_project:
|
89
|
-
rubygems_version: 1.8.
|
96
|
+
rubygems_version: 1.8.15
|
90
97
|
signing_key:
|
91
98
|
specification_version: 3
|
92
99
|
summary: Automaton and Regular Inference Toolkit
|
93
100
|
test_files: []
|
94
|
-
has_rdoc:
|