lernen 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -0
- data/README.md +531 -28
- data/Rakefile +29 -7
- data/Steepfile +14 -0
- data/examples/ripper_prism.rb +63 -0
- data/examples/uri_parse_regexp.rb +73 -0
- data/lib/lernen/algorithm/cex_processor/acex.rb +43 -0
- data/lib/lernen/algorithm/cex_processor/prefix_transformer_acex.rb +43 -0
- data/lib/lernen/algorithm/cex_processor.rb +115 -0
- data/lib/lernen/algorithm/kearns_vazirani/discrimination_tree.rb +207 -0
- data/lib/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rb +100 -0
- data/lib/lernen/algorithm/kearns_vazirani.rb +44 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rb +246 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rb +89 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa.rb +35 -0
- data/lib/lernen/algorithm/learner.rb +82 -0
- data/lib/lernen/algorithm/lsharp/lsharp_learner.rb +367 -0
- data/lib/lernen/algorithm/lsharp/observation_tree.rb +115 -0
- data/lib/lernen/algorithm/lsharp.rb +43 -0
- data/lib/lernen/algorithm/lstar/lstar_learner.rb +49 -0
- data/lib/lernen/algorithm/lstar/observation_table.rb +214 -0
- data/lib/lernen/algorithm/lstar.rb +49 -0
- data/lib/lernen/algorithm/procedural/atr_manager.rb +200 -0
- data/lib/lernen/algorithm/procedural/procedural_learner.rb +223 -0
- data/lib/lernen/algorithm/procedural/procedural_sul.rb +47 -0
- data/lib/lernen/algorithm/procedural/return_indices_acex.rb +58 -0
- data/lib/lernen/algorithm/procedural.rb +57 -0
- data/lib/lernen/algorithm.rb +19 -0
- data/lib/lernen/automaton/dfa.rb +204 -0
- data/lib/lernen/automaton/mealy.rb +108 -0
- data/lib/lernen/automaton/moore.rb +122 -0
- data/lib/lernen/automaton/moore_like.rb +83 -0
- data/lib/lernen/automaton/proc_util.rb +93 -0
- data/lib/lernen/automaton/spa.rb +368 -0
- data/lib/lernen/automaton/transition_system.rb +209 -0
- data/lib/lernen/automaton/vpa.rb +300 -0
- data/lib/lernen/automaton.rb +19 -92
- data/lib/lernen/equiv/combined_oracle.rb +57 -0
- data/lib/lernen/equiv/exhaustive_search_oracle.rb +60 -0
- data/lib/lernen/equiv/moore_like_simulator_oracle.rb +36 -0
- data/lib/lernen/equiv/oracle.rb +109 -0
- data/lib/lernen/equiv/random_walk_oracle.rb +69 -0
- data/lib/lernen/equiv/random_well_matched_word_oracle.rb +139 -0
- data/lib/lernen/equiv/random_word_oracle.rb +71 -0
- data/lib/lernen/equiv/spa_simulator_oracle.rb +39 -0
- data/lib/lernen/equiv/test_words_oracle.rb +42 -0
- data/lib/lernen/equiv/transition_system_simulator_oracle.rb +36 -0
- data/lib/lernen/equiv/vpa_simulator_oracle.rb +48 -0
- data/lib/lernen/equiv.rb +25 -0
- data/lib/lernen/graph.rb +215 -0
- data/lib/lernen/system/block_sul.rb +41 -0
- data/lib/lernen/system/moore_like_simulator.rb +45 -0
- data/lib/lernen/system/moore_like_sul.rb +33 -0
- data/lib/lernen/system/sul.rb +126 -0
- data/lib/lernen/system/transition_system_simulator.rb +40 -0
- data/lib/lernen/system.rb +72 -0
- data/lib/lernen/version.rb +2 -1
- data/lib/lernen.rb +322 -13
- data/rbs_collection.lock.yaml +16 -0
- data/rbs_collection.yaml +14 -0
- data/renovate.json +6 -0
- data/sig/generated/lernen/algorithm/cex_processor/acex.rbs +30 -0
- data/sig/generated/lernen/algorithm/cex_processor/prefix_transformer_acex.rbs +27 -0
- data/sig/generated/lernen/algorithm/cex_processor.rbs +59 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani/discrimination_tree.rbs +68 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rbs +51 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani.rbs +32 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rbs +73 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rbs +51 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa.rbs +20 -0
- data/sig/generated/lernen/algorithm/learner.rbs +53 -0
- data/sig/generated/lernen/algorithm/lsharp/lsharp_learner.rbs +103 -0
- data/sig/generated/lernen/algorithm/lsharp/observation_tree.rbs +53 -0
- data/sig/generated/lernen/algorithm/lsharp.rbs +38 -0
- data/sig/generated/lernen/algorithm/lstar/lstar_learner.rbs +38 -0
- data/sig/generated/lernen/algorithm/lstar/observation_table.rbs +79 -0
- data/sig/generated/lernen/algorithm/lstar.rbs +37 -0
- data/sig/generated/lernen/algorithm/procedural/atr_manager.rbs +80 -0
- data/sig/generated/lernen/algorithm/procedural/procedural_learner.rbs +79 -0
- data/sig/generated/lernen/algorithm/procedural/procedural_sul.rbs +36 -0
- data/sig/generated/lernen/algorithm/procedural/return_indices_acex.rbs +33 -0
- data/sig/generated/lernen/algorithm/procedural.rbs +27 -0
- data/sig/generated/lernen/algorithm.rbs +10 -0
- data/sig/generated/lernen/automaton/dfa.rbs +93 -0
- data/sig/generated/lernen/automaton/mealy.rbs +61 -0
- data/sig/generated/lernen/automaton/moore.rbs +69 -0
- data/sig/generated/lernen/automaton/moore_like.rbs +63 -0
- data/sig/generated/lernen/automaton/proc_util.rbs +38 -0
- data/sig/generated/lernen/automaton/spa.rbs +125 -0
- data/sig/generated/lernen/automaton/transition_system.rbs +108 -0
- data/sig/generated/lernen/automaton/vpa.rbs +109 -0
- data/sig/generated/lernen/automaton.rbs +15 -0
- data/sig/generated/lernen/equiv/combined_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/exhaustive_search_oracle.rbs +38 -0
- data/sig/generated/lernen/equiv/moore_like_simulator_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/oracle.rbs +75 -0
- data/sig/generated/lernen/equiv/random_walk_oracle.rbs +41 -0
- data/sig/generated/lernen/equiv/random_well_matched_word_oracle.rbs +70 -0
- data/sig/generated/lernen/equiv/random_word_oracle.rbs +45 -0
- data/sig/generated/lernen/equiv/spa_simulator_oracle.rbs +30 -0
- data/sig/generated/lernen/equiv/test_words_oracle.rbs +20 -0
- data/sig/generated/lernen/equiv/transition_system_simulator_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/vpa_simulator_oracle.rbs +33 -0
- data/sig/generated/lernen/equiv.rbs +11 -0
- data/sig/generated/lernen/graph.rbs +80 -0
- data/sig/generated/lernen/system/block_sul.rbs +29 -0
- data/sig/generated/lernen/system/moore_like_simulator.rbs +31 -0
- data/sig/generated/lernen/system/moore_like_sul.rbs +28 -0
- data/sig/generated/lernen/system/sul.rbs +87 -0
- data/sig/generated/lernen/system/transition_system_simulator.rbs +28 -0
- data/sig/generated/lernen/system.rbs +62 -0
- data/sig/generated/lernen/version.rbs +6 -0
- data/sig/generated/lernen.rbs +214 -0
- data/sig-test/generated/test/example_test.rbs +14 -0
- data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_vpa_test.rbs +10 -0
- data/sig-test/generated/test/lernen/algorithm/lsharp_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/lstar_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/procedural_test.rbs +10 -0
- data/sig-test/generated/test/lernen/automaton/dfa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/mealy_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/moore_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/proc_util_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/spa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/vpa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/equiv/exhaustive_search_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/equiv/random_walk_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/equiv/random_word_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/system/block_sul_test.rbs +16 -0
- data/sig-test/generated/test/lernen/system/moore_like_simulator_test.rbs +16 -0
- data/sig-test/generated/test/lernen/system/transition_system_simulator_test.rbs +13 -0
- data/sig-test/generated/test/lernen/system_test.rbs +11 -0
- data/sig-test/generated/test/lernen_test.rbs +13 -0
- metadata +131 -11
- data/.yardopts +0 -3
- data/lib/lernen/cex_processor.rb +0 -61
- data/lib/lernen/kearns_vazirani.rb +0 -199
- data/lib/lernen/lsharp.rb +0 -335
- data/lib/lernen/lstar.rb +0 -169
- data/lib/lernen/oracle.rb +0 -116
- data/lib/lernen/sul.rb +0 -134
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Automaton
|
6
|
+
# ProcUtil provides utility functions for words in `SPA`.
|
7
|
+
module ProcUtil
|
8
|
+
#: [In, Call, Return] (
|
9
|
+
# Return return_input,
|
10
|
+
# Array[In | Call] word,
|
11
|
+
# Hash[Call, Array[In | Call | Return]] proc_to_terminating_sequence,
|
12
|
+
# ) -> Array[In | Call | Return]
|
13
|
+
def self.expand(return_input, word, proc_to_terminating_sequence)
|
14
|
+
expanded_word = []
|
15
|
+
word.each do |input|
|
16
|
+
terminating_sequence = proc_to_terminating_sequence[input] # steep:ignore
|
17
|
+
if terminating_sequence
|
18
|
+
expanded_word << input
|
19
|
+
expanded_word.concat(terminating_sequence)
|
20
|
+
expanded_word << return_input
|
21
|
+
else
|
22
|
+
expanded_word << input
|
23
|
+
end
|
24
|
+
end
|
25
|
+
expanded_word
|
26
|
+
end
|
27
|
+
|
28
|
+
#: [In, Call, Return] (
|
29
|
+
# Set[Call] call_alphabet_set,
|
30
|
+
# Return return_input,
|
31
|
+
# Array[In | Call | Return] word
|
32
|
+
# ) -> Array[In | Call]
|
33
|
+
def self.project(call_alphabet_set, return_input, word)
|
34
|
+
projected_word = []
|
35
|
+
index = 0
|
36
|
+
while index < word.size
|
37
|
+
input = word[index]
|
38
|
+
projected_word << input
|
39
|
+
if call_alphabet_set.include?(input) # steep:ignore
|
40
|
+
return_index = find_return_index(call_alphabet_set, return_input, word, index + 1)
|
41
|
+
index = return_index if return_index
|
42
|
+
end
|
43
|
+
index += 1
|
44
|
+
end
|
45
|
+
projected_word
|
46
|
+
end
|
47
|
+
|
48
|
+
#: [In, Call, Return] (
|
49
|
+
# Set[Call] call_alphabet_set,
|
50
|
+
# Return return_input,
|
51
|
+
# Array[In | Call | Return] word,
|
52
|
+
# Integer index
|
53
|
+
# ) -> (Integer | nil)
|
54
|
+
def self.find_call_index(call_alphabet_set, return_input, word, index)
|
55
|
+
balance = 0
|
56
|
+
|
57
|
+
(index - 1).downto(0) do |i|
|
58
|
+
input = word[i]
|
59
|
+
if input == return_input # steep:ignore
|
60
|
+
balance += 1
|
61
|
+
elsif call_alphabet_set.include?(input) # steep:ignore
|
62
|
+
return i if balance == 0
|
63
|
+
balance -= 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
#: [In, Call, Return] (
|
71
|
+
# Set[Call] call_alphabet_set,
|
72
|
+
# Return return_input,
|
73
|
+
# Array[In | Call | Return] word,
|
74
|
+
# Integer index
|
75
|
+
# ) -> (Integer | nil)
|
76
|
+
def self.find_return_index(call_alphabet_set, return_input, word, index)
|
77
|
+
balance = 0
|
78
|
+
|
79
|
+
(index...word.size).each do |i|
|
80
|
+
input = word[i]
|
81
|
+
if call_alphabet_set.include?(input) # steep:ignore
|
82
|
+
balance += 1
|
83
|
+
elsif input == return_input # steep:ignore
|
84
|
+
return i if balance == 0
|
85
|
+
balance -= 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Automaton
|
6
|
+
# SPA represents a system of procedural automata.
|
7
|
+
#
|
8
|
+
# Note that this class takes `return_input` as the return symbol because
|
9
|
+
# this value is necessary to run this kind of automata correctly.
|
10
|
+
#
|
11
|
+
# @rbs generic In -- Type for input alphabet
|
12
|
+
# @rbs generic Call -- Type for call alphabet
|
13
|
+
# @rbs generic Return -- Type for return alphabet
|
14
|
+
class SPA < MooreLike #[SPA::conf[Call], In | Call | Return, bool]
|
15
|
+
# Conf is a configuration of SPA run.
|
16
|
+
#
|
17
|
+
# @rbs skip
|
18
|
+
Conf = Data.define(:prev, :proc, :state)
|
19
|
+
|
20
|
+
# @rbs!
|
21
|
+
# class Conf[Call] < Data
|
22
|
+
# attr_reader prev: conf[Call]
|
23
|
+
# attr_reader proc: Call
|
24
|
+
# attr_reader state: Integer
|
25
|
+
# def self.[]: [Call] (
|
26
|
+
# conf[Call] prev,
|
27
|
+
# Call proc,
|
28
|
+
# Integer state
|
29
|
+
# ) -> Conf[Call]
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# type conf[Call] = Conf[Call] | :init | :term | :sink
|
33
|
+
|
34
|
+
# @rbs @initial_proc: Call
|
35
|
+
# @rbs @proc_to_dfa: Hash[Call, DFA[In | Call]]
|
36
|
+
|
37
|
+
#: (
|
38
|
+
# Call initial_proc,
|
39
|
+
# Return return_input,
|
40
|
+
# Hash[Call, DFA[In | Call]] proc_to_dfa
|
41
|
+
# ) -> void
|
42
|
+
def initialize(initial_proc, return_input, proc_to_dfa)
|
43
|
+
super()
|
44
|
+
|
45
|
+
@initial_proc = initial_proc
|
46
|
+
@return_input = return_input
|
47
|
+
@proc_to_dfa = proc_to_dfa
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :initial_proc #: Call
|
51
|
+
attr_reader :return_input #: Return
|
52
|
+
attr_reader :proc_to_dfa #: Hash[Call, DFA[In | Call]]
|
53
|
+
|
54
|
+
#: () -> :spa
|
55
|
+
def type = :spa
|
56
|
+
|
57
|
+
# @rbs override
|
58
|
+
def initial_conf = :init
|
59
|
+
|
60
|
+
# @rbs override
|
61
|
+
def step_conf(conf, input)
|
62
|
+
case conf
|
63
|
+
in :init
|
64
|
+
if input == initial_proc
|
65
|
+
dfa = proc_to_dfa[initial_proc]
|
66
|
+
Conf[:term, initial_proc, dfa.initial_state]
|
67
|
+
else
|
68
|
+
:sink
|
69
|
+
end
|
70
|
+
in :term | :sink
|
71
|
+
:sink
|
72
|
+
in Conf[prev, proc, state]
|
73
|
+
dfa = proc_to_dfa[proc]
|
74
|
+
|
75
|
+
next_state = dfa.transition_function[[state, input]]
|
76
|
+
if next_state
|
77
|
+
next_dfa = proc_to_dfa[input]
|
78
|
+
if next_dfa
|
79
|
+
return_conf = Conf[prev, proc, next_state]
|
80
|
+
return Conf[return_conf, input, next_dfa.initial_state]
|
81
|
+
end
|
82
|
+
|
83
|
+
return Conf[prev, proc, next_state]
|
84
|
+
end
|
85
|
+
|
86
|
+
input == return_input && dfa.accept_state_set.include?(state) ? prev : :sink
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @rbs override
|
91
|
+
def output(conf) = conf == :term
|
92
|
+
|
93
|
+
# Checks the structural equality between `self` and `other`.
|
94
|
+
#
|
95
|
+
#: (untyped other) -> bool
|
96
|
+
def ==(other)
|
97
|
+
other.is_a?(SPA) && initial_proc == other.initial_proc && return_input == other.return_input && # steep:ignore
|
98
|
+
proc_to_dfa == other.proc_to_dfa
|
99
|
+
end
|
100
|
+
|
101
|
+
# @rbs override
|
102
|
+
def to_graph
|
103
|
+
subgraphs =
|
104
|
+
proc_to_dfa.map do |proc, dfa|
|
105
|
+
Graph::SubGraph[proc.inspect, dfa.to_graph] # steep:ignore
|
106
|
+
end
|
107
|
+
|
108
|
+
Graph.new({}, [], subgraphs)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the mapping from procedure names to access/terminating/return sequences.
|
112
|
+
#
|
113
|
+
#: (
|
114
|
+
# Array[In] alphabet,
|
115
|
+
# Array[Call] call_alphabet
|
116
|
+
# ) -> [
|
117
|
+
# Hash[Call, Array[In | Call | Return]],
|
118
|
+
# Hash[Call, Array[In | Call | Return]],
|
119
|
+
# Hash[Call, Array[In | Call | Return]]
|
120
|
+
# ]
|
121
|
+
def proc_to_atr_sequence(alphabet, call_alphabet)
|
122
|
+
proc_to_terminating_sequence = compute_proc_to_terminating_sequence(alphabet, call_alphabet)
|
123
|
+
proc_to_access_sequence, proc_to_return_sequence =
|
124
|
+
compute_proc_to_access_and_return_sequences(alphabet, call_alphabet, proc_to_terminating_sequence)
|
125
|
+
[proc_to_access_sequence, proc_to_terminating_sequence, proc_to_return_sequence]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Finds a separating word between `spa1` and `spa2`.
|
129
|
+
#
|
130
|
+
# This method assume return symbols for two SPAs are the same.
|
131
|
+
# If they are not, this raises `ArgumentError`.
|
132
|
+
#
|
133
|
+
#: [In, Call, Return] (
|
134
|
+
# Array[In] alphabet,
|
135
|
+
# Array[Call] call_alphabet,
|
136
|
+
# SPA[In, Call, Return] spa1,
|
137
|
+
# SPA[In, Call, Return] spa2
|
138
|
+
# ) -> (Array[In | Call | Return] | nil)
|
139
|
+
def self.find_separating_word(alphabet, call_alphabet, spa1, spa2)
|
140
|
+
raise ArgumentError, "Cannot find a separating word for different type automata" unless spa2.is_a?(spa1.class)
|
141
|
+
unless spa1.return_input == spa2.return_input # steep:ignore
|
142
|
+
raise ArgumentError, "Return symbols for two SPAs are different"
|
143
|
+
end
|
144
|
+
|
145
|
+
as1, ts1, rs1 = spa1.proc_to_atr_sequence(alphabet, call_alphabet)
|
146
|
+
as2, ts2, rs2 = spa2.proc_to_atr_sequence(alphabet, call_alphabet)
|
147
|
+
|
148
|
+
local_alphabet = alphabet.dup
|
149
|
+
local_alphabet.concat((ts1.keys.to_set & ts2.keys.to_set).to_a) # steep:ignore
|
150
|
+
|
151
|
+
call_alphabet.each do |proc|
|
152
|
+
dfa1 = spa1.proc_to_dfa[proc]
|
153
|
+
dfa2 = spa2.proc_to_dfa[proc]
|
154
|
+
next if !dfa1 && !dfa2
|
155
|
+
|
156
|
+
a1 = as1[proc]
|
157
|
+
t1 = ts1[proc]
|
158
|
+
r1 = rs1[proc]
|
159
|
+
|
160
|
+
a2 = as2[proc]
|
161
|
+
t2 = ts2[proc]
|
162
|
+
r2 = rs2[proc]
|
163
|
+
|
164
|
+
case
|
165
|
+
when dfa1 && !dfa2
|
166
|
+
return a1 + t1 + r1 if a1 && t1 && r1
|
167
|
+
next
|
168
|
+
when !dfa1 && dfa2
|
169
|
+
return a2 + t2 + r2 if a2 && t2 && r2
|
170
|
+
next
|
171
|
+
end
|
172
|
+
|
173
|
+
# Then, `dfa1 && dfa2` holds.
|
174
|
+
next unless a1 && t1 && r1 && a2 && t2 && r2
|
175
|
+
|
176
|
+
sep_word = DFA.find_separating_word(local_alphabet, dfa1, dfa2)
|
177
|
+
next unless sep_word
|
178
|
+
|
179
|
+
as, ts, rs = dfa1.output(dfa1.run(sep_word)[1]) ? [as1, ts1, rs1] : [as2, ts2, rs2]
|
180
|
+
sep_word = ProcUtil.expand(spa1.return_input, sep_word, ts)
|
181
|
+
return as[proc] + sep_word + rs[proc]
|
182
|
+
end
|
183
|
+
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
|
187
|
+
# Generates a SPA randomly.
|
188
|
+
#
|
189
|
+
#: [In, Call, Return] (
|
190
|
+
# alphabet: Array[In],
|
191
|
+
# call_alphabet: Array[Call],
|
192
|
+
# return_input: Return,
|
193
|
+
# ?min_proc_size: Integer,
|
194
|
+
# ?max_proc_size: Integer,
|
195
|
+
# ?dfa_min_state_size: Integer,
|
196
|
+
# ?dfa_max_state_size: Integer,
|
197
|
+
# ?dfa_accept_state_size: Integer,
|
198
|
+
# ?random: Random,
|
199
|
+
# ) -> SPA[In, Call, Return]
|
200
|
+
def self.random(
|
201
|
+
alphabet:,
|
202
|
+
call_alphabet:,
|
203
|
+
return_input:,
|
204
|
+
min_proc_size: 1,
|
205
|
+
max_proc_size: call_alphabet.size,
|
206
|
+
dfa_min_state_size: 5,
|
207
|
+
dfa_max_state_size: 10,
|
208
|
+
dfa_accept_state_size: 2,
|
209
|
+
random: Random
|
210
|
+
)
|
211
|
+
proc_size = random.rand(min_proc_size..max_proc_size)
|
212
|
+
procs = call_alphabet.dup.shuffle!(random:)[0...proc_size]
|
213
|
+
|
214
|
+
initial_proc = procs[0] # steep:ignore
|
215
|
+
proc_to_dfa = {}
|
216
|
+
procs.each do |proc| # steep:ignore
|
217
|
+
proc_to_dfa[proc] = DFA.random(
|
218
|
+
alphabet: alphabet + procs, # steep:ignore
|
219
|
+
random:,
|
220
|
+
min_state_size: dfa_min_state_size,
|
221
|
+
max_state_size: dfa_max_state_size,
|
222
|
+
accept_state_size: dfa_accept_state_size
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
new(initial_proc, return_input, proc_to_dfa)
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
# Returns the mapping from procedure names to terminating sequences.
|
232
|
+
#
|
233
|
+
#: (Array[In] alphabet, Array[Call] call_alphabet) -> Hash[Call, Array[In | Call | Return]]
|
234
|
+
def compute_proc_to_terminating_sequence(alphabet, call_alphabet)
|
235
|
+
proc_to_terminating_sequence = {}
|
236
|
+
|
237
|
+
call_alphabet.each do |proc|
|
238
|
+
dfa = proc_to_dfa[proc]
|
239
|
+
next unless dfa
|
240
|
+
terminating_sequence = dfa.shortest_accept_word(alphabet)
|
241
|
+
next unless terminating_sequence
|
242
|
+
proc_to_terminating_sequence[proc] = terminating_sequence
|
243
|
+
end
|
244
|
+
|
245
|
+
remaining_proc_set = call_alphabet.to_set
|
246
|
+
remaining_proc_set.subtract(proc_to_terminating_sequence.keys)
|
247
|
+
|
248
|
+
eligible_alphabet = alphabet.dup
|
249
|
+
eligible_alphabet.concat(proc_to_terminating_sequence.keys)
|
250
|
+
|
251
|
+
stable = false
|
252
|
+
until stable
|
253
|
+
stable = true
|
254
|
+
|
255
|
+
remaining_proc_set.each do |proc|
|
256
|
+
dfa = proc_to_dfa[proc]
|
257
|
+
|
258
|
+
unless dfa
|
259
|
+
remaining_proc_set.delete(proc)
|
260
|
+
next
|
261
|
+
end
|
262
|
+
|
263
|
+
terminating_sequence = dfa.shortest_accept_word(eligible_alphabet)
|
264
|
+
next unless terminating_sequence
|
265
|
+
|
266
|
+
proc_to_terminating_sequence[proc] = ProcUtil.expand(
|
267
|
+
return_input,
|
268
|
+
terminating_sequence,
|
269
|
+
proc_to_terminating_sequence
|
270
|
+
)
|
271
|
+
remaining_proc_set.delete(proc)
|
272
|
+
eligible_alphabet << proc # steep:ignore
|
273
|
+
|
274
|
+
stable = false
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
proc_to_terminating_sequence
|
279
|
+
end
|
280
|
+
|
281
|
+
# Returns the mapping from procedure names to access and return sequences.
|
282
|
+
#
|
283
|
+
#: (
|
284
|
+
# Array[In] alphabet,
|
285
|
+
# Array[Call] call_alphabet,
|
286
|
+
# Hash[Call, Array[In | Call | Return]] proc_to_terminating_sequence
|
287
|
+
# ) -> [Hash[Call, Array[In | Call | Return]], Hash[Call, Array[In | Call | Return]]]
|
288
|
+
def compute_proc_to_access_and_return_sequences(alphabet, call_alphabet, proc_to_terminating_sequence)
|
289
|
+
return {}, {} unless proc_to_dfa[initial_proc]
|
290
|
+
|
291
|
+
proc_to_access_sequence = {}
|
292
|
+
proc_to_return_sequence = {}
|
293
|
+
|
294
|
+
proc_to_access_sequence[initial_proc] = [initial_proc]
|
295
|
+
proc_to_return_sequence[initial_proc] = [return_input]
|
296
|
+
|
297
|
+
found_call_alphabet = [initial_proc]
|
298
|
+
unfound_call_alphabet_set = call_alphabet.to_set
|
299
|
+
unfound_call_alphabet_set.delete(initial_proc)
|
300
|
+
|
301
|
+
stable = false
|
302
|
+
until stable
|
303
|
+
stable = true
|
304
|
+
|
305
|
+
found_call_alphabet.each do |found_proc|
|
306
|
+
dfa = proc_to_dfa[found_proc]
|
307
|
+
|
308
|
+
proc_to_access_and_return_word =
|
309
|
+
explore_proc_to_access_and_return_word(dfa, alphabet, found_call_alphabet, unfound_call_alphabet_set)
|
310
|
+
proc_to_access_and_return_word.each do |proc, (i2s, n2a)|
|
311
|
+
access_sequence = []
|
312
|
+
access_sequence.concat(proc_to_access_sequence[found_proc])
|
313
|
+
access_sequence.concat(ProcUtil.expand(return_input, i2s, proc_to_terminating_sequence))
|
314
|
+
access_sequence << proc
|
315
|
+
proc_to_access_sequence[proc] = access_sequence
|
316
|
+
|
317
|
+
return_sequence = []
|
318
|
+
return_sequence << return_input
|
319
|
+
return_sequence.concat(ProcUtil.expand(return_input, n2a, proc_to_terminating_sequence))
|
320
|
+
return_sequence.concat(proc_to_return_sequence[found_proc])
|
321
|
+
proc_to_return_sequence[proc] = return_sequence
|
322
|
+
|
323
|
+
found_call_alphabet << proc
|
324
|
+
unfound_call_alphabet_set.delete(proc)
|
325
|
+
|
326
|
+
stable = false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
[proc_to_access_sequence, proc_to_return_sequence]
|
332
|
+
end
|
333
|
+
|
334
|
+
#: (
|
335
|
+
# DFA[In | Call] dfa,
|
336
|
+
# Array[In] alphabet,
|
337
|
+
# Array[Call] found_call_alphabet,
|
338
|
+
# Set[Call] unfound_call_alphabet_set
|
339
|
+
# ) -> Hash[Call, [Array[In | Call], Array[In | Call]]]
|
340
|
+
def explore_proc_to_access_and_return_word(dfa, alphabet, found_call_alphabet, unfound_call_alphabet_set)
|
341
|
+
states = dfa.states
|
342
|
+
shortest_words = dfa.compute_shortest_words(alphabet + found_call_alphabet)
|
343
|
+
|
344
|
+
proc_to_access_and_return_word = {}
|
345
|
+
unfound_call_alphabet_set.each do |proc|
|
346
|
+
found = false
|
347
|
+
states.each do |state|
|
348
|
+
next_state = dfa.transition_function[[state, proc]]
|
349
|
+
i2s = shortest_words[[dfa.initial_state, state]]
|
350
|
+
next unless i2s
|
351
|
+
|
352
|
+
dfa.accept_state_set.each do |accept_state|
|
353
|
+
n2a = shortest_words[[next_state, accept_state]]
|
354
|
+
next unless n2a
|
355
|
+
|
356
|
+
proc_to_access_and_return_word[proc] = [i2s, n2a]
|
357
|
+
found = true
|
358
|
+
break
|
359
|
+
end
|
360
|
+
break if found
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
proc_to_access_and_return_word
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Automaton
|
6
|
+
# TransitionSystem represents a labelled transition system.
|
7
|
+
#
|
8
|
+
# We assume that this transition system is *deterministic* and *complete*;
|
9
|
+
# thus, the transition function should be defined for all states and input
|
10
|
+
# characters, and the destination configuration of a transition should be one.
|
11
|
+
#
|
12
|
+
# Also, this transition system has an output value for each transition. From
|
13
|
+
# this point of view, this definition is much like Mealy machines. However,
|
14
|
+
# this class is more generic. Actually, this is a superclass of Moore machines,
|
15
|
+
# DFA, etc.
|
16
|
+
#
|
17
|
+
# Note that this class is *abstract*. We should implement the following method:
|
18
|
+
#
|
19
|
+
# - `#type`
|
20
|
+
# - `#initial_conf`
|
21
|
+
# - `#step(conf, input)`
|
22
|
+
# - `#to_graph`
|
23
|
+
#
|
24
|
+
# @rbs generic Conf -- Type for a configuration of this automaton
|
25
|
+
# @rbs generic In -- Type for input alphabet
|
26
|
+
# @rbs generic Out -- Type for output values
|
27
|
+
class TransitionSystem
|
28
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
29
|
+
|
30
|
+
# Returns the automaton type.
|
31
|
+
#
|
32
|
+
# This is an abstract method.
|
33
|
+
#
|
34
|
+
#: () -> transition_system_type
|
35
|
+
def type
|
36
|
+
raise TypeError, "abstract method: `type`"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the initial configuration.
|
40
|
+
#
|
41
|
+
# This is an abstract method.
|
42
|
+
#
|
43
|
+
#: () -> Conf
|
44
|
+
def initial_conf
|
45
|
+
raise TypeError, "abstract method: `initial_conf`"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Runs a transition from the given configuration with the given input.
|
49
|
+
#
|
50
|
+
# It returns a pair of the output value and the next configuration of
|
51
|
+
# this transition.
|
52
|
+
#
|
53
|
+
# This is an abstract method.
|
54
|
+
#
|
55
|
+
#: (Conf conf, In input) -> [Out, Conf]
|
56
|
+
def step(conf, input)
|
57
|
+
raise TypeError, "abstract method: `step`"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a graph of this transition system.
|
61
|
+
#
|
62
|
+
# This is an abstract method.
|
63
|
+
#
|
64
|
+
#: () -> Graph
|
65
|
+
def to_graph
|
66
|
+
raise TypeError, "abstract method: `to_graph`"
|
67
|
+
end
|
68
|
+
|
69
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
70
|
+
|
71
|
+
# Runs transitions from the initial configuration with the given word.
|
72
|
+
#
|
73
|
+
# It returns a pair of the output values and the final configuration of
|
74
|
+
# the transitions.
|
75
|
+
#
|
76
|
+
#: (Array[In] word) -> [Array[Out], Conf]
|
77
|
+
def run(word)
|
78
|
+
conf = initial_conf
|
79
|
+
outputs = []
|
80
|
+
word.each do |input|
|
81
|
+
output, conf = step(conf, input)
|
82
|
+
outputs << output
|
83
|
+
end
|
84
|
+
[outputs, conf]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a [Mermaid](https://mermaid.js.org) diagram of this transition system.
|
88
|
+
#
|
89
|
+
#: (?direction: Graph::mermaid_direction) -> String
|
90
|
+
def to_mermaid(direction: "TD") = to_graph.to_mermaid(direction:)
|
91
|
+
|
92
|
+
# Returns a [GraphViz](https://graphviz.org) DOT diagram of this transition system.
|
93
|
+
#
|
94
|
+
#: () -> String
|
95
|
+
def to_dot = to_graph.to_dot
|
96
|
+
|
97
|
+
# Finds a separating word between `automaton1` and `automaton2`.
|
98
|
+
#
|
99
|
+
#: [Conf, In, Out] (
|
100
|
+
# Array[In] alphabet,
|
101
|
+
# TransitionSystem[Conf, In, Out] automaton1,
|
102
|
+
# TransitionSystem[Conf, In, Out] automaton2
|
103
|
+
# ) -> (Array[In] | nil)
|
104
|
+
def self.find_separating_word(alphabet, automaton1, automaton2)
|
105
|
+
unless automaton2.is_a?(automaton1.class)
|
106
|
+
raise ArgumentError, "Cannot find a separating word for different type automata"
|
107
|
+
end
|
108
|
+
queue = []
|
109
|
+
prefix_hash = {}
|
110
|
+
|
111
|
+
initial_pair = [automaton1.initial_conf, automaton2.initial_conf]
|
112
|
+
queue << initial_pair
|
113
|
+
prefix_hash[initial_pair] = []
|
114
|
+
|
115
|
+
until queue.empty?
|
116
|
+
conf1, conf2 = queue.shift
|
117
|
+
prefix = prefix_hash[[conf1, conf2]]
|
118
|
+
|
119
|
+
alphabet.each do |input|
|
120
|
+
word = prefix + [input]
|
121
|
+
|
122
|
+
output1, next_conf1 = automaton1.step(conf1, input)
|
123
|
+
output2, next_conf2 = automaton2.step(conf2, input)
|
124
|
+
|
125
|
+
return word if output1 != output2 # steep:ignore
|
126
|
+
|
127
|
+
next_pair = [next_conf1, next_conf2]
|
128
|
+
unless prefix_hash.include?(next_pair)
|
129
|
+
queue << next_pair
|
130
|
+
prefix_hash[next_pair] = word
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
# Generates a transition function randomly.
|
139
|
+
#
|
140
|
+
# To make a transition function connected, this method generates
|
141
|
+
# a transition function in the following mannar.
|
142
|
+
#
|
143
|
+
# 1. Decide a number of states within `min_state_size..max_state_size` randomly.
|
144
|
+
# 2. Divides the states into `num_reachable_paths` partitions.
|
145
|
+
# 3. Generate a path from the initial state for each paratition.
|
146
|
+
# 4. Generate transition for all `state` and `input`.
|
147
|
+
#
|
148
|
+
# This method returns a pair of a transition function and an array of reachable paths.
|
149
|
+
# The initial state of the result transition function is `0`.
|
150
|
+
#
|
151
|
+
#: [In] (
|
152
|
+
# alphabet: Array[In],
|
153
|
+
# ?min_state_size: Integer,
|
154
|
+
# ?max_state_size: Integer,
|
155
|
+
# ?num_reachable_paths: Integer,
|
156
|
+
# ?random: Random,
|
157
|
+
# ) -> [Hash[[Integer, In], Integer], Array[Array[Integer]]]
|
158
|
+
def self.random_transition_function(
|
159
|
+
alphabet:,
|
160
|
+
min_state_size: 5,
|
161
|
+
max_state_size: 10,
|
162
|
+
num_reachable_paths: 2,
|
163
|
+
random: Random
|
164
|
+
)
|
165
|
+
state_size = random.rand(min_state_size..max_state_size)
|
166
|
+
num_reachable_paths = [num_reachable_paths, alphabet.size, state_size].min #: Integer
|
167
|
+
|
168
|
+
initial_state = 0
|
169
|
+
reachable_paths = []
|
170
|
+
transition_function = {}
|
171
|
+
|
172
|
+
partitions = (0...state_size).to_a + ([nil] * (num_reachable_paths - 1))
|
173
|
+
partitions.shuffle!(random:)
|
174
|
+
partitions.push(nil)
|
175
|
+
|
176
|
+
initial_alphabet = alphabet.dup
|
177
|
+
initial_alphabet.shuffle!(random:)
|
178
|
+
|
179
|
+
until partitions.empty?
|
180
|
+
reachable_path = [initial_state]
|
181
|
+
state = initial_state
|
182
|
+
input = initial_alphabet.shift
|
183
|
+
loop do
|
184
|
+
next_state = partitions.shift
|
185
|
+
break if next_state.nil?
|
186
|
+
next if next_state == initial_state
|
187
|
+
|
188
|
+
reachable_path << next_state
|
189
|
+
|
190
|
+
transition_function[[state, input]] = next_state
|
191
|
+
|
192
|
+
input = alphabet.sample(random:)
|
193
|
+
end
|
194
|
+
reachable_paths << reachable_path
|
195
|
+
end
|
196
|
+
|
197
|
+
state_size.times do |state|
|
198
|
+
alphabet.each do |input|
|
199
|
+
next if transition_function[[state, input]]
|
200
|
+
next_state = random.rand(state_size)
|
201
|
+
transition_function[[state, input]] = next_state
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
[transition_function, reachable_paths]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|