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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +18 -0
  3. data/README.md +531 -28
  4. data/Rakefile +29 -7
  5. data/Steepfile +14 -0
  6. data/examples/ripper_prism.rb +63 -0
  7. data/examples/uri_parse_regexp.rb +73 -0
  8. data/lib/lernen/algorithm/cex_processor/acex.rb +43 -0
  9. data/lib/lernen/algorithm/cex_processor/prefix_transformer_acex.rb +43 -0
  10. data/lib/lernen/algorithm/cex_processor.rb +115 -0
  11. data/lib/lernen/algorithm/kearns_vazirani/discrimination_tree.rb +207 -0
  12. data/lib/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rb +100 -0
  13. data/lib/lernen/algorithm/kearns_vazirani.rb +44 -0
  14. data/lib/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rb +246 -0
  15. data/lib/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rb +89 -0
  16. data/lib/lernen/algorithm/kearns_vazirani_vpa.rb +35 -0
  17. data/lib/lernen/algorithm/learner.rb +82 -0
  18. data/lib/lernen/algorithm/lsharp/lsharp_learner.rb +367 -0
  19. data/lib/lernen/algorithm/lsharp/observation_tree.rb +115 -0
  20. data/lib/lernen/algorithm/lsharp.rb +43 -0
  21. data/lib/lernen/algorithm/lstar/lstar_learner.rb +49 -0
  22. data/lib/lernen/algorithm/lstar/observation_table.rb +214 -0
  23. data/lib/lernen/algorithm/lstar.rb +49 -0
  24. data/lib/lernen/algorithm/procedural/atr_manager.rb +200 -0
  25. data/lib/lernen/algorithm/procedural/procedural_learner.rb +223 -0
  26. data/lib/lernen/algorithm/procedural/procedural_sul.rb +47 -0
  27. data/lib/lernen/algorithm/procedural/return_indices_acex.rb +58 -0
  28. data/lib/lernen/algorithm/procedural.rb +57 -0
  29. data/lib/lernen/algorithm.rb +19 -0
  30. data/lib/lernen/automaton/dfa.rb +204 -0
  31. data/lib/lernen/automaton/mealy.rb +108 -0
  32. data/lib/lernen/automaton/moore.rb +122 -0
  33. data/lib/lernen/automaton/moore_like.rb +83 -0
  34. data/lib/lernen/automaton/proc_util.rb +93 -0
  35. data/lib/lernen/automaton/spa.rb +368 -0
  36. data/lib/lernen/automaton/transition_system.rb +209 -0
  37. data/lib/lernen/automaton/vpa.rb +300 -0
  38. data/lib/lernen/automaton.rb +19 -92
  39. data/lib/lernen/equiv/combined_oracle.rb +57 -0
  40. data/lib/lernen/equiv/exhaustive_search_oracle.rb +60 -0
  41. data/lib/lernen/equiv/moore_like_simulator_oracle.rb +36 -0
  42. data/lib/lernen/equiv/oracle.rb +109 -0
  43. data/lib/lernen/equiv/random_walk_oracle.rb +69 -0
  44. data/lib/lernen/equiv/random_well_matched_word_oracle.rb +139 -0
  45. data/lib/lernen/equiv/random_word_oracle.rb +71 -0
  46. data/lib/lernen/equiv/spa_simulator_oracle.rb +39 -0
  47. data/lib/lernen/equiv/test_words_oracle.rb +42 -0
  48. data/lib/lernen/equiv/transition_system_simulator_oracle.rb +36 -0
  49. data/lib/lernen/equiv/vpa_simulator_oracle.rb +48 -0
  50. data/lib/lernen/equiv.rb +25 -0
  51. data/lib/lernen/graph.rb +215 -0
  52. data/lib/lernen/system/block_sul.rb +41 -0
  53. data/lib/lernen/system/moore_like_simulator.rb +45 -0
  54. data/lib/lernen/system/moore_like_sul.rb +33 -0
  55. data/lib/lernen/system/sul.rb +126 -0
  56. data/lib/lernen/system/transition_system_simulator.rb +40 -0
  57. data/lib/lernen/system.rb +72 -0
  58. data/lib/lernen/version.rb +2 -1
  59. data/lib/lernen.rb +322 -13
  60. data/rbs_collection.lock.yaml +16 -0
  61. data/rbs_collection.yaml +14 -0
  62. data/renovate.json +6 -0
  63. data/sig/generated/lernen/algorithm/cex_processor/acex.rbs +30 -0
  64. data/sig/generated/lernen/algorithm/cex_processor/prefix_transformer_acex.rbs +27 -0
  65. data/sig/generated/lernen/algorithm/cex_processor.rbs +59 -0
  66. data/sig/generated/lernen/algorithm/kearns_vazirani/discrimination_tree.rbs +68 -0
  67. data/sig/generated/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rbs +51 -0
  68. data/sig/generated/lernen/algorithm/kearns_vazirani.rbs +32 -0
  69. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rbs +73 -0
  70. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rbs +51 -0
  71. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa.rbs +20 -0
  72. data/sig/generated/lernen/algorithm/learner.rbs +53 -0
  73. data/sig/generated/lernen/algorithm/lsharp/lsharp_learner.rbs +103 -0
  74. data/sig/generated/lernen/algorithm/lsharp/observation_tree.rbs +53 -0
  75. data/sig/generated/lernen/algorithm/lsharp.rbs +38 -0
  76. data/sig/generated/lernen/algorithm/lstar/lstar_learner.rbs +38 -0
  77. data/sig/generated/lernen/algorithm/lstar/observation_table.rbs +79 -0
  78. data/sig/generated/lernen/algorithm/lstar.rbs +37 -0
  79. data/sig/generated/lernen/algorithm/procedural/atr_manager.rbs +80 -0
  80. data/sig/generated/lernen/algorithm/procedural/procedural_learner.rbs +79 -0
  81. data/sig/generated/lernen/algorithm/procedural/procedural_sul.rbs +36 -0
  82. data/sig/generated/lernen/algorithm/procedural/return_indices_acex.rbs +33 -0
  83. data/sig/generated/lernen/algorithm/procedural.rbs +27 -0
  84. data/sig/generated/lernen/algorithm.rbs +10 -0
  85. data/sig/generated/lernen/automaton/dfa.rbs +93 -0
  86. data/sig/generated/lernen/automaton/mealy.rbs +61 -0
  87. data/sig/generated/lernen/automaton/moore.rbs +69 -0
  88. data/sig/generated/lernen/automaton/moore_like.rbs +63 -0
  89. data/sig/generated/lernen/automaton/proc_util.rbs +38 -0
  90. data/sig/generated/lernen/automaton/spa.rbs +125 -0
  91. data/sig/generated/lernen/automaton/transition_system.rbs +108 -0
  92. data/sig/generated/lernen/automaton/vpa.rbs +109 -0
  93. data/sig/generated/lernen/automaton.rbs +15 -0
  94. data/sig/generated/lernen/equiv/combined_oracle.rbs +27 -0
  95. data/sig/generated/lernen/equiv/exhaustive_search_oracle.rbs +38 -0
  96. data/sig/generated/lernen/equiv/moore_like_simulator_oracle.rbs +27 -0
  97. data/sig/generated/lernen/equiv/oracle.rbs +75 -0
  98. data/sig/generated/lernen/equiv/random_walk_oracle.rbs +41 -0
  99. data/sig/generated/lernen/equiv/random_well_matched_word_oracle.rbs +70 -0
  100. data/sig/generated/lernen/equiv/random_word_oracle.rbs +45 -0
  101. data/sig/generated/lernen/equiv/spa_simulator_oracle.rbs +30 -0
  102. data/sig/generated/lernen/equiv/test_words_oracle.rbs +20 -0
  103. data/sig/generated/lernen/equiv/transition_system_simulator_oracle.rbs +27 -0
  104. data/sig/generated/lernen/equiv/vpa_simulator_oracle.rbs +33 -0
  105. data/sig/generated/lernen/equiv.rbs +11 -0
  106. data/sig/generated/lernen/graph.rbs +80 -0
  107. data/sig/generated/lernen/system/block_sul.rbs +29 -0
  108. data/sig/generated/lernen/system/moore_like_simulator.rbs +31 -0
  109. data/sig/generated/lernen/system/moore_like_sul.rbs +28 -0
  110. data/sig/generated/lernen/system/sul.rbs +87 -0
  111. data/sig/generated/lernen/system/transition_system_simulator.rbs +28 -0
  112. data/sig/generated/lernen/system.rbs +62 -0
  113. data/sig/generated/lernen/version.rbs +6 -0
  114. data/sig/generated/lernen.rbs +214 -0
  115. data/sig-test/generated/test/example_test.rbs +14 -0
  116. data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_test.rbs +16 -0
  117. data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_vpa_test.rbs +10 -0
  118. data/sig-test/generated/test/lernen/algorithm/lsharp_test.rbs +16 -0
  119. data/sig-test/generated/test/lernen/algorithm/lstar_test.rbs +16 -0
  120. data/sig-test/generated/test/lernen/algorithm/procedural_test.rbs +10 -0
  121. data/sig-test/generated/test/lernen/automaton/dfa_test.rbs +19 -0
  122. data/sig-test/generated/test/lernen/automaton/mealy_test.rbs +19 -0
  123. data/sig-test/generated/test/lernen/automaton/moore_test.rbs +19 -0
  124. data/sig-test/generated/test/lernen/automaton/proc_util_test.rbs +19 -0
  125. data/sig-test/generated/test/lernen/automaton/spa_test.rbs +19 -0
  126. data/sig-test/generated/test/lernen/automaton/vpa_test.rbs +19 -0
  127. data/sig-test/generated/test/lernen/equiv/exhaustive_search_oracle_test.rbs +10 -0
  128. data/sig-test/generated/test/lernen/equiv/random_walk_oracle_test.rbs +10 -0
  129. data/sig-test/generated/test/lernen/equiv/random_word_oracle_test.rbs +10 -0
  130. data/sig-test/generated/test/lernen/system/block_sul_test.rbs +16 -0
  131. data/sig-test/generated/test/lernen/system/moore_like_simulator_test.rbs +16 -0
  132. data/sig-test/generated/test/lernen/system/transition_system_simulator_test.rbs +13 -0
  133. data/sig-test/generated/test/lernen/system_test.rbs +11 -0
  134. data/sig-test/generated/test/lernen_test.rbs +13 -0
  135. metadata +131 -11
  136. data/.yardopts +0 -3
  137. data/lib/lernen/cex_processor.rb +0 -61
  138. data/lib/lernen/kearns_vazirani.rb +0 -199
  139. data/lib/lernen/lsharp.rb +0 -335
  140. data/lib/lernen/lstar.rb +0 -169
  141. data/lib/lernen/oracle.rb +0 -116
  142. 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