1
use rand::Rng;
2
use rand::RngExt;
3
use rand::seq::IndexedRandom;
4

            
5
use crate::ActDecl;
6
use crate::Assignment;
7
use crate::CommExpr;
8
use crate::DataExpr;
9
use crate::DataExprBinaryOp;
10
use crate::IdDecl;
11
use crate::MultiActionLabel;
12
use crate::ProcDecl;
13
use crate::ProcExprBinaryOp;
14
use crate::ProcessExpr;
15
use crate::Rename;
16
use crate::Sort;
17
use crate::SortExpression;
18
use crate::Span;
19
use crate::UntypedDataSpecification;
20
use crate::UntypedProcessSpecification;
21
use crate::random_boolean_data_expression;
22
use crate::random_integer_data_expression;
23

            
24
/// Generates a random linear process specification as an AST.
25
///
26
/// Models a finite-state machine: `num_states` states (encoded as a `Nat` parameter `s`),
27
/// `num_actions` label-only actions (a0, a1, …), each (state, action) pair enabled with
28
/// probability `transition_prob`, and the target state chosen uniformly. The returned
29
/// specification is accepted by `txt2lps` when printed (no linearization step required).
30
100
pub fn random_lps<R: Rng>(
31
100
    rng: &mut R,
32
100
    num_states: usize,
33
100
    num_actions: usize,
34
100
    transition_prob: f64,
35
100
) -> UntypedProcessSpecification {
36
100
    assert!(num_states >= 1 && num_actions >= 1);
37

            
38
300
    let action_names: Vec<String> = (0..num_actions).map(|i| format!("a{i}")).collect();
39

            
40
100
    let action_declarations = action_names
41
100
        .iter()
42
100
        .map(|name| ActDecl {
43
300
            identifier: name.clone(),
44
300
            args: Vec::new(),
45
300
            span: Span::default(),
46
300
        })
47
100
        .collect();
48

            
49
100
    let s_param = IdDecl::new("s".to_string(), SortExpression::Simple(Sort::Nat), Span::default());
50

            
51
100
    let mut summands: Vec<ProcessExpr> = Vec::new();
52
400
    for from in 0..num_states {
53
1200
        for act in &action_names {
54
1200
            if rng.random_bool(transition_prob) {
55
701
                let to = rng.random_range(0..num_states);
56
701
                let condition = DataExpr::Binary {
57
701
                    op: DataExprBinaryOp::Equal,
58
701
                    lhs: Box::new(DataExpr::Id("s".to_string())),
59
701
                    rhs: Box::new(DataExpr::Number(from.to_string())),
60
701
                };
61
701
                let seq = ProcessExpr::Binary {
62
701
                    op: ProcExprBinaryOp::Sequence,
63
701
                    lhs: Box::new(ProcessExpr::Action(act.clone(), Vec::new())),
64
701
                    rhs: Box::new(ProcessExpr::Id(
65
701
                        "P".to_string(),
66
701
                        vec![Assignment {
67
701
                            identifier: "s".to_string(),
68
701
                            expr: DataExpr::Number(to.to_string()),
69
701
                        }],
70
701
                    )),
71
701
                };
72
701
                summands.push(ProcessExpr::Condition {
73
701
                    condition,
74
701
                    then: Box::new(seq),
75
701
                    else_: None,
76
701
                });
77
701
            }
78
        }
79
    }
80

            
81
    // Keep the spec syntactically valid when no transitions are generated.
82
100
    if summands.is_empty() {
83
        summands.push(ProcessExpr::Delta);
84
100
    }
85

            
86
100
    let body = summands
87
100
        .into_iter()
88
100
        .reduce(|acc, s| ProcessExpr::Binary {
89
601
            op: ProcExprBinaryOp::Choice,
90
601
            lhs: Box::new(acc),
91
601
            rhs: Box::new(s),
92
601
        })
93
100
        .expect("summands is non-empty");
94

            
95
100
    let process_declarations = vec![ProcDecl {
96
100
        identifier: "P".to_string(),
97
100
        params: vec![s_param],
98
100
        body,
99
100
        span: Span::default(),
100
100
    }];
101

            
102
100
    let init_state = rng.random_range(0..num_states);
103
100
    let init = ProcessExpr::Id(
104
100
        "P".to_string(),
105
100
        vec![Assignment {
106
100
            identifier: "s".to_string(),
107
100
            expr: DataExpr::Number(init_state.to_string()),
108
100
        }],
109
100
    );
110

            
111
100
    UntypedProcessSpecification {
112
100
        data_specification: UntypedDataSpecification::default(),
113
100
        global_variables: Vec::new(),
114
100
        action_declarations,
115
100
        process_declarations,
116
100
        init: Some(init),
117
100
    }
118
100
}
119

            
120
const ACTIONS: &[&str] = &["a", "b", "c", "d"];
121
const PROC_NAMES: &[&str] = &["P", "Q", "R"];
122

            
123
// Sum-variable names are kept distinct from process parameter names (b, c, m, n).
124
const SUM_VARS: &[&str] = &["s1", "s2", "s3"];
125

            
126
10210
fn id_decl(name: &str, sort: Sort) -> IdDecl {
127
10210
    IdDecl::new(name.to_string(), SortExpression::Simple(sort), Span::default())
128
10210
}
129

            
130
3930
fn is_bool(decl: &IdDecl) -> bool {
131
3930
    matches!(&decl.sort, SortExpression::Simple(Sort::Bool))
132
3930
}
133

            
134
struct ProcVar {
135
    name: String,
136
    params: Vec<IdDecl>,
137
}
138

            
139
300
fn random_proc_var<R: Rng>(rng: &mut R, index: usize, use_integers: bool) -> ProcVar {
140
300
    let size = rng.random_range(0..=2usize);
141
300
    let mut pool: Vec<IdDecl> = vec![id_decl("b", Sort::Bool), id_decl("c", Sort::Bool)];
142
300
    if use_integers {
143
141
        pool.push(id_decl("m", Sort::Nat));
144
141
        pool.push(id_decl("n", Sort::Nat));
145
159
    }
146
300
    let mut params = Vec::new();
147
300
    for _ in 0..size {
148
321
        if pool.is_empty() {
149
            break;
150
321
        }
151
321
        let idx = rng.random_range(0..pool.len());
152
321
        params.push(pool.remove(idx));
153
    }
154
300
    ProcVar {
155
300
        name: PROC_NAMES[index].to_string(),
156
300
        params,
157
300
    }
158
300
}
159

            
160
69
fn random_process_instance<R: Rng>(rng: &mut R, pv: &ProcVar, freevars: &[IdDecl]) -> ProcessExpr {
161
69
    let assignments = pv
162
69
        .params
163
69
        .iter()
164
72
        .map(|p| {
165
72
            let expr = if is_bool(p) {
166
52
                random_boolean_data_expression(rng, freevars)
167
            } else {
168
20
                random_integer_data_expression(rng, freevars)
169
            };
170
72
            Assignment {
171
72
                identifier: p.identifier.clone(),
172
72
                expr,
173
72
            }
174
72
        })
175
69
        .collect();
176
69
    ProcessExpr::Id(pv.name.clone(), assignments)
177
69
}
178

            
179
742
fn random_leaf<R: Rng>(
180
742
    rng: &mut R,
181
742
    freevars: &[IdDecl],
182
742
    actions: &[&str],
183
742
    proc_vars: &[ProcVar],
184
742
    is_guarded: bool,
185
742
) -> ProcessExpr {
186
    // Build a weighted table: Action has high weight, Delta/Tau low, ProcessInstance medium.
187
742
    let mut table: Vec<usize> = vec![0, 1]; // Delta=0, Tau=1
188
742
    if !actions.is_empty() {
189
742
        table.extend(std::iter::repeat_n(2, 8));
190
742
    }
191

            
192
742
    if !proc_vars.is_empty() && !is_guarded {
193
413
        table.extend(std::iter::repeat_n(3, 2)); // ProcessInstance=3
194
413
    }
195

            
196
742
    match *table.choose(rng).expect("table always contains at least Delta and Tau") {
197
74
        0 => ProcessExpr::Delta,
198
75
        1 => ProcessExpr::Tau,
199
524
        2 => ProcessExpr::Action(
200
524
            (*actions.choose(rng).expect("actions is non-empty")).to_string(),
201
524
            Vec::new(),
202
524
        ),
203
        3 => {
204
69
            let pv = proc_vars.choose(rng).expect("proc_vars is non-empty");
205
69
            random_process_instance(rng, pv, freevars)
206
        }
207
        _ => unreachable!(),
208
    }
209
742
}
210

            
211
1856
fn random_process_expr<R: Rng>(
212
1856
    rng: &mut R,
213
1856
    depth: usize,
214
1856
    freevars: &[IdDecl],
215
1856
    actions: &[&str],
216
1856
    proc_vars: &[ProcVar],
217
1856
    is_guarded: bool,
218
1856
) -> ProcessExpr {
219
1856
    if depth == 0 {
220
453
        return random_leaf(rng, freevars, actions, proc_vars, is_guarded);
221
1403
    }
222

            
223
    // op: 0=leaf, 1=sum, 2=if-then, 3=if-then-else, 4=choice, 5=seq
224
    // seq is over-represented to bias toward action-guarded continuations.
225
1403
    let op_table: &[usize] = &[0, 0, 1, 2, 3, 4, 4, 5, 5, 5];
226
1403
    match *op_table.choose(rng).expect("op_table is a non-empty constant") {
227
289
        0 => random_leaf(rng, freevars, actions, proc_vars, is_guarded),
228
        1 => {
229
            // Sum: bind a fresh variable chosen from SUM_VARS to avoid capture.
230
139
            let var_name = SUM_VARS
231
139
                .iter()
232
249
                .find(|&&n| !freevars.iter().any(|v| v.identifier == n))
233
139
                .copied();
234
139
            match var_name {
235
                None => random_leaf(rng, freevars, actions, proc_vars, is_guarded),
236
139
                Some(name) => {
237
139
                    let sort = if rng.random_bool(0.5) { Sort::Bool } else { Sort::Nat };
238
139
                    let var = id_decl(name, sort);
239
139
                    let mut new_vars = freevars.to_vec();
240
139
                    new_vars.push(var.clone());
241
139
                    let body = random_process_expr(rng, depth - 1, &new_vars, actions, proc_vars, is_guarded);
242
139
                    ProcessExpr::Sum {
243
139
                        variables: vec![var],
244
139
                        operand: Box::new(body),
245
139
                    }
246
                }
247
            }
248
        }
249
        2 => {
250
            // IfThen: condition -> body
251
145
            let cond = random_boolean_data_expression(rng, freevars);
252
145
            let body = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, is_guarded);
253
145
            ProcessExpr::Condition {
254
145
                condition: cond,
255
145
                then: Box::new(body),
256
145
                else_: None,
257
145
            }
258
        }
259
        3 => {
260
            // IfThenElse: condition -> x <> y
261
146
            let cond = random_boolean_data_expression(rng, freevars);
262
146
            let then = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, is_guarded);
263
146
            let else_ = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, is_guarded);
264
146
            ProcessExpr::Condition {
265
146
                condition: cond,
266
146
                then: Box::new(then),
267
146
                else_: Some(Box::new(else_)),
268
146
            }
269
        }
270
        4 => {
271
            // Choice: lhs + rhs (each branch must independently satisfy guardedness)
272
296
            let lhs = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, is_guarded);
273
296
            let rhs = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, is_guarded);
274
296
            ProcessExpr::Binary {
275
296
                op: ProcExprBinaryOp::Choice,
276
296
                lhs: Box::new(lhs),
277
296
                rhs: Box::new(rhs),
278
296
            }
279
        }
280
        5 => {
281
            // Seq: emit an action first, then an (unguarded) continuation.
282
            // The explicit action satisfies the guard, so the rhs may reference proc instances.
283
388
            let action = (*actions.choose(rng).expect("actions is non-empty")).to_string();
284
388
            let lhs = ProcessExpr::Action(action, Vec::new());
285
388
            let rhs = random_process_expr(rng, depth - 1, freevars, actions, proc_vars, false);
286
388
            ProcessExpr::Binary {
287
388
                op: ProcExprBinaryOp::Sequence,
288
388
                lhs: Box::new(lhs),
289
388
                rhs: Box::new(rhs),
290
388
            }
291
        }
292
        _ => unreachable!(),
293
    }
294
1856
}
295

            
296
266
fn apply_wrapper<R: Rng>(rng: &mut R, actions: &[&str], expr: ProcessExpr) -> ProcessExpr {
297
266
    match rng.random_range(0..5usize) {
298
        0 => {
299
58
            let a = (*actions.choose(rng).expect("actions is non-empty")).to_string();
300
58
            ProcessExpr::Hide {
301
58
                actions: vec![a],
302
58
                operand: Box::new(expr),
303
58
            }
304
        }
305
        1 => {
306
51
            let a = (*actions.choose(rng).expect("actions is non-empty")).to_string();
307
51
            ProcessExpr::Block {
308
51
                actions: vec![a],
309
51
                operand: Box::new(expr),
310
51
            }
311
        }
312
50
        2 if actions.len() >= 2 => {
313
50
            let mut pool = actions.to_vec();
314
50
            let ai = rng.random_range(0..pool.len());
315
50
            let from = pool.remove(ai).to_string();
316
50
            let to = (*pool
317
50
                .choose(rng)
318
50
                .expect("pool has at least one element after removing from"))
319
50
            .to_string();
320
50
            ProcessExpr::Rename {
321
50
                renames: vec![Rename { from, to }],
322
50
                operand: Box::new(expr),
323
50
            }
324
        }
325
63
        3 if actions.len() >= 3 => {
326
            // Comm: a | b -> c  (mCRL2 synchronisation mapping)
327
252
            let mut pool: Vec<String> = actions.iter().map(|&s| s.to_string()).collect();
328
63
            let ai = rng.random_range(0..pool.len());
329
63
            let a = pool.remove(ai);
330
63
            let bi = rng.random_range(0..pool.len());
331
63
            let b = pool.remove(bi);
332
63
            let c = pool
333
63
                .choose(rng)
334
63
                .expect("pool has at least one element after removing a and b")
335
63
                .clone();
336
63
            ProcessExpr::Comm {
337
63
                comm: vec![CommExpr::new(MultiActionLabel::new(vec![a, b]), c)],
338
63
                operand: Box::new(expr),
339
63
            }
340
        }
341
        4 => {
342
44
            let mut labels: Vec<MultiActionLabel> = (0..5)
343
220
                .map(|_| {
344
220
                    let size = rng.random_range(1..=2usize);
345
220
                    let mut acts: Vec<String> = (0..size)
346
330
                        .map(|_| (*actions.choose(rng).expect("actions is non-empty")).to_string())
347
220
                        .collect();
348
220
                    acts.sort();
349
220
                    acts.dedup();
350
220
                    MultiActionLabel::new(acts)
351
220
                })
352
44
                .collect();
353
44
            labels.sort();
354
44
            labels.dedup();
355
44
            ProcessExpr::Allow {
356
44
                actions: labels,
357
44
                operand: Box::new(expr),
358
44
            }
359
        }
360
        _ => expr, // guard failures from arms 2/3 fall here
361
    }
362
266
}
363

            
364
100
fn random_parallel_init<R: Rng>(
365
100
    rng: &mut R,
366
100
    actions: &[&str],
367
100
    mut procs: Vec<ProcessExpr>,
368
100
    wrapper_count: usize,
369
100
) -> ProcessExpr {
370
    // Fold all instances into a single parallel composition tree.
371
300
    while procs.len() > 1 {
372
200
        let n = procs.len();
373
200
        let j = rng.random_range(1..n);
374
200
        let p = procs.remove(j);
375
200
        let q = procs.remove(0);
376
200
        procs.push(ProcessExpr::Binary {
377
200
            op: ProcExprBinaryOp::Parallel,
378
200
            lhs: Box::new(q),
379
200
            rhs: Box::new(p),
380
200
        });
381
200
    }
382
100
    let mut result = procs.remove(0);
383
266
    for _ in 0..wrapper_count {
384
266
        result = apply_wrapper(rng, actions, result);
385
266
    }
386
100
    result
387
100
}
388

            
389
/// Generates a random mCRL2 process specification.
390
///
391
/// `equation_count` controls how many process equations are produced (capped at 3).
392
/// `depth` controls the maximum nesting depth of each process body.
393
/// `use_integers` adds `Nat`-typed parameters alongside `Bool` ones.
394
100
pub fn make_process_specification<R: Rng>(
395
100
    rng: &mut R,
396
100
    equation_count: usize,
397
100
    depth: usize,
398
100
    use_integers: bool,
399
100
) -> UntypedProcessSpecification {
400
100
    let count = equation_count.min(PROC_NAMES.len());
401
300
    let proc_vars: Vec<ProcVar> = (0..count).map(|i| random_proc_var(rng, i, use_integers)).collect();
402

            
403
100
    let action_declarations = ACTIONS
404
100
        .iter()
405
100
        .map(|&name| ActDecl {
406
400
            identifier: name.to_string(),
407
400
            args: Vec::new(),
408
400
            span: Span::default(),
409
400
        })
410
100
        .collect();
411

            
412
100
    let process_declarations: Vec<ProcDecl> = proc_vars
413
100
        .iter()
414
300
        .map(|pv| {
415
300
            let body = random_process_expr(rng, depth, &pv.params, ACTIONS, &proc_vars, true);
416
300
            ProcDecl {
417
300
                identifier: pv.name.clone(),
418
300
                params: pv.params.clone(),
419
300
                body,
420
300
                span: Span::default(),
421
300
            }
422
300
        })
423
100
        .collect();
424

            
425
100
    let instances: Vec<ProcessExpr> = proc_vars
426
100
        .iter()
427
300
        .map(|pv| {
428
300
            let assignments = pv
429
300
                .params
430
300
                .iter()
431
321
                .map(|p| {
432
321
                    let expr = if is_bool(p) {
433
245
                        DataExpr::Bool(rng.random_bool(0.5))
434
                    } else {
435
76
                        DataExpr::Number(rng.random_range(0..=2u32).to_string())
436
                    };
437
321
                    Assignment {
438
321
                        identifier: p.identifier.clone(),
439
321
                        expr,
440
321
                    }
441
321
                })
442
300
                .collect();
443
300
            ProcessExpr::Id(pv.name.clone(), assignments)
444
300
        })
445
100
        .collect();
446

            
447
100
    let init = if instances.is_empty() {
448
        ProcessExpr::Delta
449
    } else {
450
100
        let wrapper_count = rng.random_range(0..=5usize);
451
100
        random_parallel_init(rng, ACTIONS, instances, wrapper_count)
452
    };
453

            
454
100
    UntypedProcessSpecification {
455
100
        data_specification: UntypedDataSpecification::default(),
456
100
        global_variables: Vec::new(),
457
100
        action_declarations,
458
100
        process_declarations,
459
100
        init: Some(init),
460
100
    }
461
100
}