-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprogram.mbt
More file actions
372 lines (356 loc) · 13.1 KB
/
program.mbt
File metadata and controls
372 lines (356 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
///|
/// One generated symbolic command, its symbolic response, and fresh variables.
///
/// `command` is the action the generator chose. `response` is the mocked
/// symbolic response used to advance the symbolic model before the real system
/// has run. `vars` records which variables were introduced by that response so
/// shrink validation can remap dependencies later.
pub(all) struct Command[CSym, RSym] {
command : CSym
response : RSym
vars : Array[Var]
} derive(Eq, Debug)
///|
/// A symbolic command program.
///
/// This is the stable artifact that can be shrunk, formatted, saved, loaded,
/// and replayed. It intentionally stores symbolic responses, not concrete
/// responses, because those are what make the program self-contained before
/// execution.
pub(all) struct Commands[CSym, RSym] {
commands : Array[Command[CSym, RSym]]
} derive(Eq, Debug)
///|
/// Constructs an empty command program.
pub fn[CSym, RSym] Commands::empty() -> Commands[CSym, RSym] {
{ commands: [] }
}
///|
/// Returns the number of generated commands.
pub fn[CSym, RSym] Commands::length(self : Commands[CSym, RSym]) -> Int {
self.commands.length()
}
///|
/// Appends one command while preserving the original program.
pub fn[CSym, RSym] Commands::append(
self : Commands[CSym, RSym],
command : Command[CSym, RSym],
) -> Commands[CSym, RSym] {
let commands = self.commands.copy()
commands.push(command)
{ commands, }
}
///|
/// Concatenates two command programs.
pub fn[CSym, RSym] Commands::concat(
left : Commands[CSym, RSym],
right : Commands[CSym, RSym],
) -> Commands[CSym, RSym] {
let commands = left.commands.copy()
for command in right.commands {
commands.push(command)
}
{ commands, }
}
///|
/// Configuration shared by generation, execution, coverage checks, and
/// shrinking.
pub(all) struct RunConfig {
// Base seed. `check` derives case seeds by adding the case index.
seed : UInt64
// Number of independent generated programs to test.
cases : Int
// Maximum length of each generated command program.
max_commands : Int
// Size hint passed to the user generator.
size : Int
// Number of times generation may retry after a precondition rejects a command.
max_tries : Int
// Whether `check` should minimize a failing generated program.
shrink : Bool
// Maximum number of accepted shrink steps.
max_shrinks : Int
// Maximum number of shrink rounds. Each round starts from the current best
// failing program and searches for one smaller failing candidate.
max_shrink_rounds : Int
// Semantic coverage labels that must appear in a successful run.
required_labels : Array[String]
// Command families that must appear in a successful run.
required_command_names : Array[String]
} derive(Eq, Debug)
///|
/// Conservative defaults suitable for examples and small model tests.
pub fn RunConfig::default() -> RunConfig {
{
seed: 37,
cases: 100,
max_commands: 50,
size: 100,
max_tries: 100,
shrink: true,
max_shrinks: 100,
max_shrink_rounds: 20,
required_labels: [],
required_command_names: [],
}
}
///|
/// `Default` implementation forwarding to `RunConfig::default`.
pub impl Default for RunConfig with fn default() {
RunConfig::default()
}
///|
/// Failures that can occur while generating a symbolic command program.
pub(all) enum GenerationFailure[MSym, CSym, RSym] {
// The `RunConfig` cannot be used, for example because a limit is negative.
InvalidGenerationConfig(String)
// The generator kept producing commands rejected by the precondition. This
// usually means the model has reached a terminal state that should return
// `None`, or the generator is not respecting the current model.
Deadlock(
step_index~ : Int,
model~ : MSym,
commands~ : Commands[CSym, RSym],
last_precondition~ : Logic?
)
} derive(Eq, Debug)
///|
/// Complete state-machine specification.
///
/// This record is the MoonBit counterpart of QSM's `StateMachine`: users supply
/// the pure model operations, symbolic/reference plumbing, and real semantics;
/// the library supplies generation, replay, shrinking, and reporting.
///
/// The type separates symbolic and concrete model/command/response types:
/// symbolic callbacks run before the system exists, while concrete callbacks run
/// against the implementation under test.
pub struct StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S] {
// Initial model used by generation and shrinking.
init_symbolic_model : () -> MSym
// Initial model used by replay against the concrete system.
init_concrete_model : () -> MCon
// Creates the mutable system under test for one replay.
init_system : () -> S raise
// Proposes the next symbolic command from the current symbolic model.
// Returning `None` stops generation cleanly.
generator : (MSym, Int, @splitmix.RandomState) -> CSym?
// Predicts the symbolic response so generation can advance the model before
// execution. This is where commands that create resources allocate `Var`s.
mock : (MSym, CSym, GenSym) -> (RSym, GenSym)
// Extracts variables introduced by a symbolic response, in positional order.
response_vars : (RSym) -> Array[Var]
// Advances the symbolic model during generation and shrink validation.
transition_symbolic : (MSym, CSym, RSym) -> MSym
// Advances the concrete model after executing a concrete command.
transition_concrete : (MCon, CCon, RCon) -> MCon
// Rejects commands that are not valid in the current symbolic model.
precondition : (MSym, CSym) -> Logic
// Checks the observed concrete response against the model.
postcondition : (MCon, CCon, RCon) -> Logic
// Checks model-wide properties after each transition.
invariant : (MCon) -> Logic
// Produces smaller versions of one symbolic command.
shrinker : (MSym, CSym) -> Array[CSym]
// Rewrites symbolic variables after earlier commands have been deleted or
// rebuilt during shrinking.
remap_command : (CSym, Map[Var, Var]) -> CSym?
// Turns symbolic references into concrete values using the environment.
reify_command : (CSym, Environment[V]) -> Result[CCon, EnvError]
// Executes the concrete command against the mutable system under test.
run_command : (CCon, S) -> RCon raise
// Binds concrete values returned by the real response to variables predicted
// by the symbolic response.
bind_response : (RSym, RCon, Environment[V]) -> Result[
Environment[V],
BindError,
]
// Releases resources after a replay, successful or not.
cleanup : (MCon, S) -> Unit raise
// Human-readable command family name used for coverage reports.
command_name : (CSym) -> String
// Semantic labels collected from concrete execution for coverage reports.
label : (MCon, CCon, RCon) -> Array[String]
}
///|
/// Builds a `StateMachine` with conservative defaults for optional callbacks.
///
/// Required callbacks describe the core state-machine loop: initialize models
/// and system, generate symbolic commands, mock responses, transition models,
/// reify commands, run the system, and bind newly produced concrete values.
/// Optional callbacks default to permissive behavior so small examples can
/// start with only the essential pieces.
pub fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] StateMachine::new(
init_symbolic_model~ : () -> MSym,
init_concrete_model~ : () -> MCon,
init_system~ : () -> S raise,
generator~ : (MSym, Int, @splitmix.RandomState) -> CSym?,
mock~ : (MSym, CSym, GenSym) -> (RSym, GenSym),
transition_symbolic~ : (MSym, CSym, RSym) -> MSym,
transition_concrete~ : (MCon, CCon, RCon) -> MCon,
reify_command~ : (CSym, Environment[V]) -> Result[CCon, EnvError],
run_command~ : (CCon, S) -> RCon raise,
bind_response~ : (RSym, RCon, Environment[V]) -> Result[
Environment[V],
BindError,
],
postcondition? : (MCon, CCon, RCon) -> Logic = (_, _, _) => Logic::top(),
precondition? : (MSym, CSym) -> Logic = (_, _) => Logic::top(),
invariant? : (MCon) -> Logic = _ => Logic::top(),
response_vars? : (RSym) -> Array[Var] = _ => [],
shrinker? : (MSym, CSym) -> Array[CSym] = (_, _) => [],
remap_command? : (CSym, Map[Var, Var]) -> CSym? = (command, _) => {
Some(command)
},
cleanup? : (MCon, S) -> Unit raise = (_, _) => (),
command_name? : (CSym) -> String = _ => "command",
label? : (MCon, CCon, RCon) -> Array[String] = (_, _, _) => [],
) -> StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S] {
{
init_symbolic_model,
init_concrete_model,
init_system,
generator,
mock,
response_vars,
transition_symbolic,
transition_concrete,
precondition,
postcondition,
invariant,
shrinker,
remap_command,
reify_command,
run_command,
bind_response,
cleanup,
command_name,
label,
}
}
///|
// Validates limits before generation or replay starts. Replay uses the same
// config because coverage checks and shrinking limits are part of the run too.
fn validate_generation_config(config : RunConfig) -> String? {
if config.cases < 0 {
Some("cases must be non-negative")
} else if config.max_commands < 0 {
Some("max_commands must be non-negative")
} else if config.size < 0 {
Some("size must be non-negative")
} else if config.max_tries <= 0 {
Some("max_tries must be positive")
} else if config.max_shrinks < 0 {
Some("max_shrinks must be non-negative")
} else if config.max_shrink_rounds < 0 {
Some("max_shrink_rounds must be non-negative")
} else {
None
}
}
///|
// Internal result of asking the generator for one valid symbolic command.
priv enum Selection[CSym] {
// The generator explicitly stopped this program.
Stop
// A command satisfied the precondition.
Selected(CSym)
// The retry budget was exhausted by rejected commands. The final
// precondition is retained for diagnostics.
Exhausted(Logic?)
}
///|
// Calls the user generator until it either stops, produces a command accepted by
// the precondition, or exhausts `max_tries`.
fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] select_symbolic_command(
spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S],
model : MSym,
size : Int,
rng : @splitmix.RandomState,
max_tries : Int,
) -> Selection[CSym] {
for tries = 0, last_precondition = None; tries < max_tries; {
match (spec.generator)(model, size, rng) {
None => break Stop
Some(command) => {
let precondition = (spec.precondition)(model, command)
if precondition.eval() {
break Selected(command)
} else {
continue tries + 1, Some(precondition)
}
}
}
} nobreak {
Exhausted(last_precondition)
}
}
///|
/// Generates one symbolic command program using an explicit seed.
///
/// Generation follows the QSM symbolic loop: select a precondition-valid
/// command, ask `mock` for the symbolic response, record response variables,
/// advance the symbolic model, and repeat until `max_commands` or generator
/// termination.
pub fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] generate_commands_with_seed(
spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S],
config? : RunConfig = RunConfig::default(),
seed? : UInt64 = config.seed,
) -> Result[Commands[CSym, RSym], GenerationFailure[MSym, CSym, RSym]] {
match validate_generation_config(config) {
Some(message) => Err(InvalidGenerationConfig(message))
None => {
let rng = @splitmix.new(seed~)
let initial_commands : Commands[CSym, RSym] = Commands::empty()
for step_index = 0, model = (spec.init_symbolic_model)(), gen_sym = GenSym::new(), commands = initial_commands; step_index <
config.max_commands; {
match
select_symbolic_command(
spec,
model,
config.size,
rng,
config.max_tries,
) {
Stop => break Ok(commands)
Exhausted(last_precondition) =>
break Err(
Deadlock(step_index~, model~, commands~, last_precondition~),
)
Selected(symbolic_command) => {
// The mocked response is part of the generated program. It gives
// later symbolic commands stable variables to mention and keeps the
// symbolic model in lockstep with the command prefix.
let (symbolic_response, next_gen_sym) = (spec.mock)(
model, symbolic_command, gen_sym,
)
let vars = (spec.response_vars)(symbolic_response)
let command = Command::{
command: symbolic_command,
response: symbolic_response,
vars,
}
let next_model = (spec.transition_symbolic)(
model,
command.command,
command.response,
)
continue step_index + 1,
next_model,
next_gen_sym,
commands.append(command)
}
}
} nobreak {
Ok(commands)
}
}
}
}
///|
/// Generates one symbolic command program using `config.seed`.
pub fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] generate_commands(
spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S],
config? : RunConfig = RunConfig::default(),
) -> Result[Commands[CSym, RSym], GenerationFailure[MSym, CSym, RSym]] {
generate_commands_with_seed(spec, config~)
}