1
use itertools::Itertools;
2
use log::info;
3
use streaming_iterator::StreamingIterator;
4

            
5
use merc_collections::VecBag;
6
use merc_io::TimeProgress;
7
use merc_lts::LTS;
8
use merc_lts::LabelIndex;
9
use merc_lts::LtsAction;
10
use merc_lts::LtsBuilder;
11
use merc_lts::LtsMultiAction;
12
use merc_lts::SimpleAction;
13
use merc_lts::StateIndex;
14
use merc_lts::Transition;
15
use merc_lts::TransitionLabel;
16
use merc_syntax::CommExpr;
17
use merc_syntax::MultiActionLabel;
18
use merc_utilities::MercError;
19
use merc_utilities::Timing;
20

            
21
use crate::DiscoveredSet;
22
use crate::SequenceForestContext;
23
use crate::StateRef;
24

            
25
/// The trait for labels used in the composition operator.
26
pub trait CombineLabel: TransitionLabel + Ord {
27
    /// Returns true iff the data arguments of two actions are compatible for communication.
28
    /// Defaults to `true` for label types without structured arguments (e.g., `String`).
29
    fn comm_args_compatible(&self, _other: &Self) -> bool {
30
        true
31
    }
32

            
33
    /// Creates a new action as the result of a communication expression with the given `label`.
34
    /// `representative` is one of the matched input actions and provides argument values.
35
    fn from_comm(label: String, representative: &Self) -> Self;
36
}
37

            
38
impl CombineLabel for SimpleAction {
39
4
    fn comm_args_compatible(&self, other: &Self) -> bool {
40
4
        self.arguments() == other.arguments()
41
4
    }
42

            
43
2
    fn from_comm(label: String, representative: &Self) -> Self {
44
2
        SimpleAction::new(label, representative.arguments().to_vec())
45
2
    }
46
}
47

            
48
impl CombineLabel for LtsAction {
49
    fn comm_args_compatible(&self, other: &Self) -> bool {
50
        self.arguments() == other.arguments()
51
    }
52

            
53
    fn from_comm(label: String, representative: &Self) -> Self {
54
        LtsAction::new(label, representative.arguments().to_vec())
55
    }
56
}
57

            
58
/// Computes the parallel composition hide(H, allow(A, comm(C, L1 || ... || Ln))).
59
///
60
/// The `builder` is used to construct the resulting LTS, which can also be
61
/// stored immediately in a file.
62
pub fn combine_lts<L: LTS<Label = LtsMultiAction<A>>, A: CombineLabel, B: LtsBuilder<L::Label>>(
63
    builder: &mut B,
64
    parallel_composition: Vec<L>,
65
    hide: &[String],
66
    allow: &[MultiActionLabel],
67
    comm: &[CommExpr],
68
    timing: &Timing,
69
) -> Result<(), MercError> {
70
    if parallel_composition.is_empty() {
71
        return Err("At least one LTS is required for composition.".into());
72
    }
73

            
74
    if allow.iter().any(|allow| allow.is_tau_label()) {
75
        return Err("Allow set cannot contain the tau action.".into());
76
    }
77

            
78
    if comm.iter().any(|comm| comm.from.is_tau_label()) {
79
        return Err("Communication expressions cannot have tau actions on the left-hand side.".into());
80
    }
81

            
82
    for (i, comm_i) in comm.iter().enumerate() {
83
        if comm_i.from.actions.len() < 2 {
84
            return Err(format!(
85
                "Communication expressions must have at least two actions on the left-hand side, but {comm_i} does not."
86
            )
87
            .into());
88
        }
89

            
90
        // The left hand side of a communication cannot overlap with any other communication's left hand side. This is the definition used in mCRL2,
91
        // for example a|b -> a is a valid communication expression.
92
        for (j, comm_j) in comm.iter().enumerate() {
93
            if i != j && comm_i.from.actions.iter().any(|a| comm_j.from.actions.contains(a)) {
94
                return Err(format!(
95
                    "Communication expressions cannot have overlapping left-hand sides, i.e. {comm_i} and {comm_j}."
96
                )
97
                .into());
98
            }
99
        }
100

            
101
        // The right hand side of a communication cannot be in another communication's left hand side.
102
        for (j, comm_j) in comm.iter().enumerate() {
103
            if i != j && comm_j.from.actions.contains(&comm_i.to) {
104
                return Err(
105
                    format!("Communication expressions cannot have right-hand side in another communication's left-hand side, i.e. {comm_i} and {comm_j}.").into(),
106
                );
107
            }
108
        }
109
    }
110

            
111
    // Keep track of the discovered states in the combined LTS. State vectors
112
    // are stored as maximally shared sequences of raw `usize` indices in the
113
    // discovered set; we convert to/from `StateIndex` at the boundary.
114
    let discovered: DiscoveredSet<usize> = DiscoveredSet::new();
115
    let initial_vector: Vec<usize> = parallel_composition
116
        .iter()
117
        .map(|lts| lts.initial_state_index().value())
118
        .collect();
119
    let (initial_ref, _) = discovered.insert(&initial_vector);
120

            
121
    let progress = TimeProgress::new(
122
        |(states, transitions): (usize, usize)| {
123
            info!("Explored {states} states, {transitions} transitions...");
124
        },
125
        1,
126
    );
127

            
128
    // Pre-sort the allow set for efficient matching.
129
    let sorted_allow: Vec<SortedMultiActionLabel> = allow.iter().map(SortedMultiActionLabel::new).collect();
130

            
131
    // The working set of states that must still be explored.
132
    let mut working: Vec<StateRef> = vec![initial_ref];
133
    let mut iter_context = ParallelTransitionContext::new();
134

            
135
    // Reusable buffers for reconstructing the current state vector from the
136
    // discovered set and for building target vectors prior to insertion.
137
    let mut current_state_raw: Vec<usize> = Vec::new();
138
    let mut current_state_vector: Vec<StateIndex> = Vec::new();
139
    let mut target_raw: Vec<usize> = Vec::new();
140
    // Reused interning scratch buffers, avoiding a reallocation per inserted
141
    // state. This loop is single-threaded, so one context suffices.
142
    let mut forest_context = SequenceForestContext::new();
143

            
144
    timing.measure("compose", || -> Result<(), MercError> {
145
        while let Some(current) = working.pop() {
146
            // Reconstruct the current state vector from the discovered set.
147
            debug_assert!(
148
                discovered.get_into(current, &mut current_state_raw),
149
                "StateRef from working set must be valid"
150
            );
151
            current_state_vector.clear();
152
            current_state_vector.extend(current_state_raw.iter().copied().map(StateIndex::new));
153

            
154
            // Loop over all subsets of LTSs and their outgoing transitions in the current state vector.
155
            let mut iter = ParallelTransitionIter::new(&mut iter_context, &parallel_composition, &current_state_vector);
156
            loop {
157
                iter.advance();
158
                let Some(transition) = iter.get() else {
159
                    break;
160
                };
161

            
162
                // Build the combined multi-action alpha = alpha_{j_0} | ... | alpha_{j_m}.
163
                let mut actions = VecBag::new();
164
                for (k, &lts_idx) in transition.subset_indices.iter().enumerate() {
165
                    let label_idx = transition.labels[k];
166
                    let label = &parallel_composition[lts_idx].labels()[label_idx];
167
                    for action in label.actions() {
168
                        actions.insert(action.clone());
169
                    }
170
                }
171
                let multi_action = LtsMultiAction::new(actions);
172

            
173
                // Apply communication: alpha = gamma_C(alpha).
174
                let multi_action = communicate(comm, multi_action);
175

            
176
                // Check allow: alpha in A ∪ {tau}.
177
                if !is_allowed(&sorted_allow, &SortedLtsMultiAction::new(&multi_action)) {
178
                    continue;
179
                }
180

            
181
                // Apply hide: alpha = tau_I(alpha).
182
                let multi_action = hide_action(hide, multi_action);
183

            
184
                // Convert the target vector into raw indices for the discovered set.
185
                target_raw.clear();
186
                target_raw.extend(transition.target.iter().map(|s| s.value()));
187

            
188
                let (target_ref, is_new) = discovered.insert_with(&target_raw, &mut forest_context);
189
                let to = StateIndex::new(target_ref.index());
190
                builder.add_transition(StateIndex::new(current.index()), &multi_action, to)?;
191

            
192
                if is_new {
193
                    working.push(target_ref);
194
                }
195
            }
196

            
197
            progress.print((discovered.len(), builder.num_of_transitions()));
198
        }
199

            
200
        Ok(())
201
    })?;
202

            
203
    info!(
204
        "Composition complete: {} states, {} transitions",
205
        discovered.len(),
206
        builder.num_of_transitions()
207
    );
208

            
209
    builder.finish(StateIndex::new(initial_ref.index()))?;
210
    Ok(())
211
}
212

            
213
/// Applies the communication operator to a multi-action according to the given
214
/// communication expressions.
215
///
216
/// # Details
217
/// Applies the communication operator $\gamma_C$ to a multi-action.
218
///
219
/// For each communication expression $a_1 | \cdots | a_n \rightarrow c$ in $C$,
220
/// repeatedly finds matching sub-multisets of actions (with equal arguments)
221
/// and replaces them with the result action $c$.
222
fn communicate<L: CombineLabel>(comm: &[CommExpr], action: LtsMultiAction<L>) -> LtsMultiAction<L> {
223
    let mut actions = action.into_actions().to_vec();
224

            
225
    for expr in comm {
226
        // Try to find a matching sub-multiset for this communication expression.
227
        while let Some(replacement) = find_communication_match(&actions, expr) {
228
            actions = replacement;
229
        }
230
    }
231

            
232
    LtsMultiAction::new(VecBag::from_vec(actions))
233
}
234

            
235
/// Tries to find actions matching a single communication expression
236
/// $a_1 | \cdots | a_n \rightarrow c$ within the given action list.
237
///
238
/// Returns the resulting action list with the matched actions replaced by the
239
/// communicated action, or `None` if no match was found.
240
3
fn find_communication_match<L: CombineLabel>(actions: &[L], expr: &CommExpr) -> Option<Vec<L>> {
241
    // For each action name in the communication expression's left-hand side,
242
    // find a matching action with the same label. All matched actions must
243
    // have the same arguments.
244
    //
245
    // We iterate over every candidate for the *first* required name so that we
246
    // do not greedily commit to a particular argument set.  Once the first
247
    // action is chosen its arguments fix the constraint for all remaining
248
    // names, so a single greedy pass over the rest is correct.
249
3
    let mut matched = vec![false; actions.len()];
250
3
    let first_name = expr.from.actions.first()?;
251

            
252
5
    'outer: for first_idx in 0..actions.len() {
253
5
        if !actions[first_idx].matches_label(first_name) {
254
1
            continue;
255
4
        }
256

            
257
        // Attempt to complete the match using actions[first_idx] as the
258
        // argument reference.
259
4
        matched.fill(false);
260
4
        matched[first_idx] = true;
261

            
262
4
        for required_name in expr.from.actions.iter().skip(1) {
263
4
            let mut found = false;
264
10
            for (i, action) in actions.iter().enumerate() {
265
10
                if matched[i] {
266
4
                    continue;
267
6
                }
268
6
                if action.matches_label(required_name) && action.comm_args_compatible(&actions[first_idx]) {
269
2
                    matched[i] = true;
270
2
                    found = true;
271
2
                    break;
272
4
                }
273
            }
274
4
            if !found {
275
2
                continue 'outer;
276
2
            }
277
        }
278

            
279
        // Build the result: remove matched actions and add the communicated action.
280
2
        let mut result: Vec<L> = actions
281
2
            .iter()
282
2
            .enumerate()
283
5
            .filter(|(i, _)| !matched[*i])
284
2
            .map(|(_, a)| a.clone())
285
2
            .collect();
286

            
287
2
        result.push(L::from_comm(expr.to.clone(), &actions[first_idx]));
288
2
        return Some(result);
289
    }
290

            
291
1
    None
292
3
}
293

            
294
/// Returns true iff the given multi-action is allowed by the allow operator.
295
///
296
/// A multi-action is allowed if:
297
/// - The action is tau (always allowed), or
298
/// - The action label names, compared as sorted multisets, match one of the
299
///   entries in the allow set.
300
6
fn is_allowed<L: CombineLabel>(allow: &[SortedMultiActionLabel], action: &SortedLtsMultiAction<L>) -> bool {
301
6
    if action.is_tau_label() {
302
1
        return true;
303
5
    }
304

            
305
5
    allow.iter().any(|allowed| {
306
5
        allowed.actions.len() == action.len()
307
4
            && allowed
308
4
                .actions
309
4
                .iter()
310
4
                .zip(action.iter_actions())
311
9
                .all(|(name, a)| a.matches_label(name))
312
5
    })
313
6
}
314

            
315
/// Applies the hide operator $\tau_I$ to a multi-action.
316
///
317
/// Removes all actions whose label is in the hide set $I$. If all actions are
318
/// hidden the result is the tau action (empty multi-action).
319
fn hide_action<L: CombineLabel>(hide: &[String], mut action: LtsMultiAction<L>) -> LtsMultiAction<L> {
320
    if !hide.is_empty() {
321
        action.retain(|a| !hide.iter().any(|h| a.matches_label(h)));
322
    }
323
    action
324
}
325

            
326
/// Reusable scratch buffers for [`CartesianProduct`].
327
///
328
/// Holding these buffers in a long-lived context avoids re-allocating per
329
/// iteration.
330
struct CartesianProductContext {
331
    /// The length of each factor.
332
    lengths: Vec<usize>,
333
    /// Current index into each factor.
334
    indices: Vec<usize>,
335
}
336

            
337
impl CartesianProductContext {
338
    /// Creates a fresh context with empty buffers.
339
    pub(crate) fn new() -> Self {
340
        Self {
341
            lengths: Vec::new(),
342
            indices: Vec::new(),
343
        }
344
    }
345

            
346
    /// Advances the element by one step.
347
    ///
348
    /// Returns `Some(i)` where `i` is the leftmost position that was
349
    /// incremented (all positions to its right were reset to 0). Returns
350
    /// `None` when the product is exhausted.
351
    fn advance_element(&mut self) -> Option<usize> {
352
        for i in (0..self.indices.len()).rev() {
353
            self.indices[i] += 1;
354
            if self.indices[i] < self.lengths[i] {
355
                return Some(i);
356
            }
357
            self.indices[i] = 0;
358
        }
359
        None
360
    }
361
}
362

            
363
impl Default for CartesianProductContext {
364
    fn default() -> Self {
365
        Self::new()
366
    }
367
}
368

            
369
/// A multi-action label with action names sorted, for proper multiset
370
/// comparison with [`SortedLtsMultiAction`].
371
struct SortedMultiActionLabel {
372
    /// Action names in sorted order.
373
    actions: Vec<String>,
374
}
375

            
376
impl SortedMultiActionLabel {
377
6
    fn new(label: &MultiActionLabel) -> Self {
378
6
        let mut actions = label.actions.clone();
379
6
        actions.sort();
380
6
        SortedMultiActionLabel { actions }
381
6
    }
382
}
383

            
384
/// A view of an [`LtsMultiAction`] for multiset comparison with
385
/// [`SortedMultiActionLabel`].
386
///
387
/// `VecBag<L>` iterates in `L`'s sorted order, this only works if `L`'s `Ord`
388
/// implementation is consistent with the label-matching logic in `is_allowed`
389
/// and the communication matching in `find_communication_match`.
390
struct SortedLtsMultiAction<'a, L: Ord> {
391
    action: &'a LtsMultiAction<L>,
392
}
393

            
394
impl<'a, L: TransitionLabel> SortedLtsMultiAction<'a, L> {
395
6
    fn new(action: &'a LtsMultiAction<L>) -> Self {
396
        // Check if the L Ord is consistent.
397
6
        debug_assert!(
398
6
            action
399
6
                .actions()
400
6
                .iter()
401
6
                .zip(action.actions().iter().sorted())
402
13
                .all(|(a, b)| a == b),
403
            "LtsMultiAction's internal order must be consistent with the label-matching logic"
404
        );
405

            
406
6
        SortedLtsMultiAction { action }
407
6
    }
408

            
409
6
    fn is_tau_label(&self) -> bool {
410
6
        self.action.is_tau_label()
411
6
    }
412

            
413
4
    fn iter_actions(&self) -> impl Iterator<Item = &L> {
414
4
        self.action.actions().iter()
415
4
    }
416

            
417
5
    fn len(&self) -> usize {
418
5
        self.action.actions().len()
419
5
    }
420
}
421

            
422
/// The output of a parallel transition step: label indices from participating
423
/// LTSs and the combined target state vector.
424
struct ParallelTransition {
425
    /// Indices of LTSs participating in this transition's subset.
426
    pub subset_indices: Vec<usize>,
427
    /// Label indices from each participating LTS in the current subset.
428
    pub labels: Vec<LabelIndex>,
429
    /// Target state vector: for LTSs in the subset, the transition target;
430
    /// for others, the current state is retained.
431
    pub target: Vec<StateIndex>,
432
}
433

            
434
/// Reusable scratch buffers for [`ParallelTransitionIter`].
435
///
436
/// Holding these buffers in a long-lived context avoids re-allocating per
437
/// source state during a single `combine_lts` invocation.
438
struct ParallelTransitionContext {
439
    /// Indices of LTSs participating in the current subset.
440
    subset_indices: Vec<usize>,
441
    /// Snapshot of the current source state vector (one state per LTS).
442
    base_target: Vec<StateIndex>,
443
    /// Current transition for each LTS in `subset_indices`, in the same order.
444
    current: Vec<Transition>,
445
    /// Cartesian product context tracking the current offset and length for
446
    /// each LTS in `subset_indices`, in the same order.
447
    cartesian_product_context: CartesianProductContext,
448
    /// Output buffer surfaced via `StreamingIterator::get`.
449
    result: ParallelTransition,
450
}
451

            
452
impl ParallelTransitionContext {
453
    /// Creates a fresh context with empty buffers.
454
    pub(crate) fn new() -> Self {
455
        Self {
456
            subset_indices: Vec::new(),
457
            base_target: Vec::new(),
458
            current: Vec::new(),
459
            cartesian_product_context: CartesianProductContext::new(),
460
            result: ParallelTransition {
461
                subset_indices: Vec::new(),
462
                labels: Vec::new(),
463
                target: Vec::new(),
464
            },
465
        }
466
    }
467
}
468

            
469
impl Default for ParallelTransitionContext {
470
    fn default() -> Self {
471
        Self::new()
472
    }
473
}
474

            
475
/// A streaming iterator that lazily enumerates all parallel transitions from a
476
/// given state vector across multiple LTSs.
477
///
478
/// For each non-empty subset $J \subseteq \{0, \ldots, n-1\}$ of LTSs,
479
/// enumerates the Cartesian product of outgoing transitions from the LTSs in
480
/// $J$ without materialising all outgoing transitions ahead of time. Internal
481
/// buffers live in a [`ParallelTransitionContext`] that can be reused across
482
/// many source states.
483
struct ParallelTransitionIter<'ctx, 'a, L: LTS> {
484
    ctx: &'ctx mut ParallelTransitionContext,
485
    lts_list: &'a [L],
486
    /// Current subset bitmask (1 to 2^n - 1).
487
    current_subset: usize,
488
    /// Upper bound for subset enumeration (2^n).
489
    max_subset: usize,
490
    done: bool,
491
    started: bool,
492
}
493

            
494
impl<'ctx, 'a, L: LTS> ParallelTransitionIter<'ctx, 'a, L> {
495
    /// Creates a new iterator over parallel transitions for the given LTSs
496
    /// and current state vector, reusing the buffers held in `ctx`.
497
    pub(crate) fn new(
498
        ctx: &'ctx mut ParallelTransitionContext,
499
        lts_list: &'a [L],
500
        current_states: &[StateIndex],
501
    ) -> Self {
502
        assert!(
503
            lts_list.len() < usize::BITS as usize,
504
            "Number of LTSs exceeds maximum supported for subset enumeration"
505
        );
506
        assert_eq!(
507
            lts_list.len(),
508
            current_states.len(),
509
            "LTS slice and state vector must have the same length"
510
        );
511

            
512
        // Reset all reusable buffers while keeping their allocated capacity.
513
        ctx.subset_indices.clear();
514
        ctx.current.clear();
515
        ctx.cartesian_product_context.indices.clear();
516
        ctx.cartesian_product_context.lengths.clear();
517

            
518
        ctx.base_target.clear();
519
        ctx.base_target.extend_from_slice(current_states);
520

            
521
        ctx.result.subset_indices.clear();
522
        ctx.result.labels.clear();
523
        ctx.result.target.clear();
524
        ctx.result.target.extend_from_slice(current_states);
525

            
526
        Self {
527
            ctx,
528
            lts_list,
529
            current_subset: 1,
530
            max_subset: 1usize << lts_list.len(),
531
            done: false,
532
            started: false,
533
        }
534
    }
535

            
536
    /// Sets up the Cartesian product context and initial transitions for the
537
    /// current subset bitmask.
538
    ///
539
    /// Returns `true` if the subset has a non-empty Cartesian product (every
540
    /// participating LTS has at least one outgoing transition from its
541
    /// current state).
542
    fn setup_subset(&mut self) -> bool {
543
        let ctx = &mut *self.ctx;
544
        ctx.subset_indices.clear();
545
        ctx.current.clear();
546
        ctx.cartesian_product_context.indices.clear();
547
        ctx.cartesian_product_context.lengths.clear();
548

            
549
        for i in 0..self.lts_list.len() {
550
            if self.current_subset & (1 << i) != 0 {
551
                let state = ctx.base_target[i];
552
                let mut iter = self.lts_list[i].outgoing_transitions(state);
553
                let Some(first) = iter.next() else {
554
                    return false;
555
                };
556
                let len = 1 + iter.count();
557
                ctx.subset_indices.push(i);
558
                ctx.current.push(first);
559
                ctx.cartesian_product_context.indices.push(0);
560
                ctx.cartesian_product_context.lengths.push(len);
561
            }
562
        }
563
        true
564
    }
565

            
566
    /// Advances the Cartesian product odometer for the current subset.
567
    ///
568
    /// Returns `true` if a new combination is available; `false` if the
569
    /// subset is exhausted.
570
    fn advance_product(&mut self) -> bool {
571
        let ctx = &mut *self.ctx;
572
        // Advance the shared odometer; get the leftmost position that changed.
573
        // All positions to its right were reset to 0.
574
        let Some(changed_from) = ctx.cartesian_product_context.advance_element() else {
575
            return false;
576
        };
577
        // Re-fetch transitions only for the positions whose offset changed.
578
        for k in changed_from..ctx.subset_indices.len() {
579
            let lts_idx = ctx.subset_indices[k];
580
            let state = ctx.base_target[lts_idx];
581
            let offset = ctx.cartesian_product_context.indices[k];
582
            ctx.current[k] = self.lts_list[lts_idx]
583
                .outgoing_transitions(state)
584
                .nth(offset)
585
                .expect("Offset is within the iterator's exact length");
586
        }
587
        true
588
    }
589

            
590
    /// Materialises the next result into the context's output buffer.
591
    fn fill_result(&mut self) {
592
        let ctx = &mut *self.ctx;
593
        let subset = &ctx.subset_indices;
594
        let current = &ctx.current;
595
        let base = &ctx.base_target;
596
        let result = &mut ctx.result;
597

            
598
        result.subset_indices.clear();
599
        result.subset_indices.extend_from_slice(subset);
600
        result.labels.clear();
601
        result.target.clear();
602
        result.target.extend_from_slice(base);
603

            
604
        for (k, &lts_idx) in subset.iter().enumerate() {
605
            let t = &current[k];
606
            result.labels.push(t.label);
607
            result.target[lts_idx] = t.to;
608
        }
609
    }
610
}
611

            
612
impl<'a, L: LTS> StreamingIterator for ParallelTransitionIter<'_, 'a, L> {
613
    type Item = ParallelTransition;
614

            
615
    fn advance(&mut self) {
616
        if self.done {
617
            return;
618
        }
619

            
620
        // Try to advance within the current Cartesian product.
621
        if self.started {
622
            if self.advance_product() {
623
                self.fill_result();
624
                return;
625
            }
626
            self.current_subset += 1;
627
        }
628
        self.started = true;
629

            
630
        // Find the next subset with a non-empty Cartesian product.
631
        while self.current_subset < self.max_subset {
632
            if self.setup_subset() {
633
                self.fill_result();
634
                return;
635
            }
636
            self.current_subset += 1;
637
        }
638

            
639
        self.done = true;
640
    }
641

            
642
    fn get(&self) -> Option<&Self::Item> {
643
        if self.done || !self.started {
644
            None
645
        } else {
646
            Some(&self.ctx.result)
647
        }
648
    }
649
}
650

            
651
#[cfg(test)]
652
mod tests {
653
    use merc_collections::VecBag;
654
    use merc_lts::LtsMultiAction;
655
    use merc_lts::SimpleAction;
656
    use merc_syntax::CommExpr;
657
    use merc_syntax::MultiActionLabel;
658

            
659
    use super::SortedLtsMultiAction;
660
    use super::SortedMultiActionLabel;
661
    use super::find_communication_match;
662
    use super::is_allowed;
663

            
664
24
    fn simple(label: &str, args: &[&str]) -> SimpleAction {
665
24
        SimpleAction::new(label.to_owned(), args.iter().map(|s| s.to_string()).collect())
666
24
    }
667

            
668
3
    fn comm(from: &[&str], to: &str) -> CommExpr {
669
3
        CommExpr::new(
670
6
            MultiActionLabel::new(from.iter().map(|s| s.to_string()).collect()),
671
3
            to.to_owned(),
672
        )
673
3
    }
674

            
675
    /// Builds the allow set from label-name multisets.
676
6
    fn allow(entries: &[&[&str]]) -> Vec<SortedMultiActionLabel> {
677
6
        entries
678
6
            .iter()
679
6
            .map(|names| {
680
14
                SortedMultiActionLabel::new(&MultiActionLabel::new(names.iter().map(|s| s.to_string()).collect()))
681
6
            })
682
6
            .collect()
683
6
    }
684

            
685
    /// Builds a multi-action from actions; `VecBag` orders them internally.
686
6
    fn multi_action(actions: Vec<SimpleAction>) -> LtsMultiAction<SimpleAction> {
687
6
        LtsMultiAction::new(VecBag::from_vec(actions))
688
6
    }
689

            
690
6
    fn is_allowed_by(entries: &[&[&str]], actions: Vec<SimpleAction>) -> bool {
691
6
        let action = multi_action(actions);
692
6
        is_allowed(&allow(entries), &SortedLtsMultiAction::new(&action))
693
6
    }
694

            
695
    /// The allow operator matches on label-name multisets only, independent of
696
    /// arguments. This locks in the name-primary ordering that [`is_allowed`]
697
    /// relies on: here `a`'s argument sorts *after* `b`'s, so an argument-first
698
    /// ordering of the bag would misalign the zip and wrongly reject `{a, b}`.
699
    #[test]
700
1
    fn allow_matches_multiset_ignoring_arguments() {
701
        // Arguments chosen so an argument-first sort would reorder the names.
702
1
        assert!(is_allowed_by(
703
1
            &[&["a", "b"]],
704
1
            vec![simple("a", &["9"]), simple("b", &["1"])]
705
        ));
706

            
707
        // Repeated names compare as a multiset, not a set.
708
1
        assert!(is_allowed_by(
709
1
            &[&["a", "a", "b"]],
710
1
            vec![simple("a", &["1"]), simple("a", &["2"]), simple("b", &[])]
711
        ));
712
1
        assert!(!is_allowed_by(
713
1
            &[&["a", "b", "b"]],
714
1
            vec![simple("a", &["1"]), simple("a", &["2"]), simple("b", &[])]
715
1
        ));
716

            
717
        // A differing length or name is rejected; tau is always allowed.
718
1
        assert!(!is_allowed_by(
719
1
            &[&["a", "b"]],
720
1
            vec![simple("a", &[]), simple("b", &[]), simple("b", &[])]
721
1
        ));
722
1
        assert!(!is_allowed_by(&[&["a", "c"]], vec![simple("a", &[]), simple("b", &[])]));
723
1
        assert!(is_allowed_by(&[&["a", "b"]], vec![]));
724
1
    }
725

            
726
    /// Regression: when the first candidate for the first required name has
727
    /// incompatible args, the function must try subsequent candidates.
728
    ///
729
    /// Multi-action: a(1) | a(2) | b(2)
730
    /// Comm: a | b -> c
731
    ///
732
    /// Greedy first-pick would choose a(1), then fail to match b because
733
    /// b(2) has different args.  The correct match is a(2) | b(2) -> c(2).
734
    #[test]
735
1
    fn first_candidate_incompatible_retries() {
736
1
        let actions = vec![simple("a", &["1"]), simple("a", &["2"]), simple("b", &["2"])];
737
1
        let expr = comm(&["a", "b"], "c");
738

            
739
1
        let result = find_communication_match(&actions, &expr);
740
1
        assert!(result.is_some(), "should find a(2)|b(2) -> c(2)");
741

            
742
1
        let result = result.unwrap();
743
        // Remaining multi-action: a(1) and c(2)  (a(2) and b(2) consumed)
744
1
        assert_eq!(result.len(), 2);
745
1
        assert!(result.iter().any(|a| a == &simple("a", &["1"])));
746
2
        assert!(result.iter().any(|a| a == &simple("c", &["2"])));
747
1
    }
748

            
749
    /// When all candidates are genuinely incompatible, the function should
750
    /// still return `None`.
751
    #[test]
752
1
    fn no_compatible_pair_returns_none() {
753
1
        let actions = vec![simple("a", &["1"]), simple("b", &["2"])];
754
1
        let expr = comm(&["a", "b"], "c");
755

            
756
1
        assert!(find_communication_match(&actions, &expr).is_none());
757
1
    }
758

            
759
    /// Basic case with no data args still works.
760
    #[test]
761
1
    fn no_args_basic_comm() {
762
1
        let actions = vec![simple("a", &[]), simple("b", &[])];
763
1
        let expr = comm(&["a", "b"], "c");
764

            
765
1
        let result = find_communication_match(&actions, &expr).unwrap();
766
1
        assert_eq!(result.len(), 1);
767
1
        assert_eq!(result[0], simple("c", &[]));
768
1
    }
769
}