Skip to main content

wasmtime_environ/component/
same_vmctx.rs

1//! Analysis of which of a core module's function imports must always have the
2//! same `vmctx`.
3//!
4//! Calling an imported function requires passing the callee's `vmctx`, which is
5//! loaded from that import's slot in the `VMFunctionImport` array. Imports that
6//! are always satisfied by functions from the same instance always hold the
7//! same pointer, so compiling all of them to load from the lowest-numbered such
8//! slot lets GVN collapse those loads, and every subsequent load hanging off
9//! the callee context, into one.
10//!
11//! This is a *must* analysis: putting two imports in one set claims that they
12//! share a `vmctx` in *every* instantiation. Splitting sets apart is always
13//! sound; merging them when there is some possible scenario that they aren't
14//! the same `vmctx` is unsound.
15//!
16//! Each core module's lattice is the same-vmctx partitions of its function
17//! imports, ordered by refinement. Top is the single set containing everything,
18//! bottom is all singletons, and meet is the coarsest common refinement. Every
19//! module starts at top, so one that is never instantiated stays there
20//! vacuously. Meet is applied once per instantiation, and once per module that
21//! escapes to the host, which may instantiate it with anything. See
22//! `SameVmctxPartition`.
23//!
24//! An instantiation's arguments are always exports of instances created before
25//! it, so the DFG is a DAG and one pass reaches the fixpoint.
26
27use crate::compile::ModuleTranslation;
28use crate::component::ExportItem;
29use crate::component::dfg::{
30    AdapterId, AdapterModuleId, ComponentDfg, CoreDef, Export, Instance, InstanceId, SideEffect,
31};
32use crate::prelude::*;
33use crate::union_find::UnionFind;
34use crate::{
35    EntityIndex, EntityRef, FuncIndex, ImportPositions, PrimaryMap, SecondaryMap, StaticModuleIndex,
36};
37use core::mem;
38use std::collections::HashMap;
39use std::collections::hash_map::Entry;
40
41/// An identity for the `vmctx` that a core definition's `VMFuncRef` carries.
42///
43/// Equal keys guarantee equal `vmctx` pointers at runtime.
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
45enum VmctxKey {
46    /// The component's own `VMComponentContext`.
47    ///
48    /// Every trampoline and unsafe intrinsic takes its `VMFuncRef::vmctx` from
49    /// the component's single `ComponentInstance`.
50    Component,
51
52    /// The `VMContext` of a particular core Wasm instance in this component.
53    CoreInstance(InstanceId),
54
55    /// The `VMContext` of a particular adapter module's instance.
56    ///
57    /// Adapter modules are instantiated at most once each, so the module id
58    /// also names the instance.
59    AdapterModule(AdapterModuleId),
60}
61
62/// A partition of one core module's function imports into sets that must share
63/// a `vmctx`.
64#[derive(Clone, Debug, Default)]
65enum SameVmctxPartition {
66    /// One set containing every function import: they all share a `vmctx`.
67    ///
68    /// The lattice's top element, and every module's initial state.
69    #[default]
70    Top,
71
72    /// Any other partition.
73    ///
74    /// Function imports absent from the union-find are singletons, so an empty
75    /// union-find is all singletons: the lattice's bottom element.
76    Known(UnionFind<FuncIndex>),
77}
78
79impl SameVmctxPartition {
80    /// The lattice's bottom element: every function import in its own
81    /// singleton set, so nothing is known.
82    fn bottom() -> Self {
83        SameVmctxPartition::Known(UnionFind::new())
84    }
85
86    /// Build the partition induced by one instantiation, where `keys[i]` is
87    /// the `vmctx` key of the definition satisfying the module's `i`th
88    /// function import.
89    ///
90    /// `None` represents "unknown" and makes that import a singleton.
91    ///
92    /// `first` is scratch space, reused across calls to avoid reallocating a
93    /// hash map per instantiation.
94    fn from_keys(first: &mut HashMap<VmctxKey, FuncIndex>, keys: &[Option<VmctxKey>]) -> Self {
95        first.clear();
96        let mut sets = UnionFind::new();
97        for (i, key) in keys.iter().enumerate() {
98            let Some(key) = *key else { continue };
99            let func = FuncIndex::new(i);
100            match first.entry(key) {
101                Entry::Occupied(e) => {
102                    sets.union(*e.get(), func);
103                }
104                Entry::Vacant(e) => {
105                    e.insert(func);
106                }
107            }
108        }
109        SameVmctxPartition::Known(sets)
110    }
111
112    /// The coarsest common refinement of `a` and `b`: the greatest lower
113    /// bound in this lattice.
114    ///
115    /// `groups` is scratch space, reused across calls to avoid reallocating a
116    /// hash map per meet.
117    fn meet(groups: &mut HashMap<(FuncIndex, FuncIndex), FuncIndex>, a: Self, b: Self) -> Self {
118        use SameVmctxPartition::*;
119        match (a, b) {
120            // Top is the identity.
121            (Top, p) | (p, Top) => p,
122
123            (Known(ref a), Known(ref b)) => {
124                // Two imports share a block of the result exactly when they
125                // share a block of `a` *and* a block of `b`, so an import that
126                // is a singleton in either operand is a singleton in the
127                // result. Only imports that are non-singletons in *both* need
128                // looking at, which is why this walks the smaller operand and
129                // probes the larger: `O(min(|a|, |b|))`, not anything
130                // proportional to the number of function imports.
131                let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
132                let mut sets = UnionFind::new();
133
134                // Group by the pair of blocks an element belongs to, then
135                // union together everything that lands in the same group.
136                groups.clear();
137                for func in small.elems() {
138                    if !large.contains(func) {
139                        continue;
140                    }
141                    let group = (
142                        a.find_without_path_compression(func),
143                        b.find_without_path_compression(func),
144                    );
145                    match groups.entry(group) {
146                        Entry::Occupied(e) => {
147                            sets.union(*e.get(), func);
148                        }
149                        Entry::Vacant(e) => {
150                            e.insert(func);
151                        }
152                    }
153                }
154
155                Known(sets)
156            }
157        }
158    }
159
160    /// Canonicalize to the lowest-numbered function import in `func`'s set,
161    /// whose `vmctx` slot every member of the set can share.
162    ///
163    /// Lowest-numbered canonicalization ultimately allows for better codegen,
164    /// because the constant can fit in fewer instructions and/or smaller
165    /// immediate encodings.
166    fn representative(&self, func: FuncIndex) -> FuncIndex {
167        match self {
168            // One set of everything, whose least member is import 0.
169            SameVmctxPartition::Top => FuncIndex::new(0),
170
171            // We rely on the union-find caching a set's min, rather than
172            // walking the set's elements to find the min on demand, to avoid
173            // accidentally-quadratic runtimes.
174            SameVmctxPartition::Known(sets) => sets.set_min(func),
175        }
176    }
177}
178
179/// Accumulates observations to produce a `SameVmctxImports`.
180#[derive(Default)]
181struct SameVmctxBuilder {
182    /// The current lattice state of each static module.
183    ///
184    /// An unobserved module reads as `Top`.
185    partitions: SecondaryMap<StaticModuleIndex, SameVmctxPartition>,
186
187    /// Scratch space for `SameVmctxPartition::from_keys` so that its allocation
188    /// is reused across instantiations.
189    scratch_first: HashMap<VmctxKey, FuncIndex>,
190
191    /// Scratch space for `SameVmctxPartition::meet` so that its allocation is
192    /// reused across meets.
193    scratch_groups: HashMap<(FuncIndex, FuncIndex), FuncIndex>,
194}
195
196impl SameVmctxBuilder {
197    /// Record an instantiation of `module` in which the definition satisfying
198    /// its `i`th function import has `vmctx` key `keys[i]`.
199    fn observe_instantiation(&mut self, module: StaticModuleIndex, keys: &[Option<VmctxKey>]) {
200        let partition = SameVmctxPartition::from_keys(&mut self.scratch_first, keys);
201        self.observe(module, partition);
202    }
203
204    /// Record that `module` may also be instantiated in ways we cannot see,
205    /// for example because the component exports it to the host.
206    fn observe_unknown_instantiation(&mut self, module: StaticModuleIndex) {
207        self.observe(module, SameVmctxPartition::bottom());
208    }
209
210    fn observe(&mut self, module: StaticModuleIndex, partition: SameVmctxPartition) {
211        let current = mem::take(&mut self.partitions[module]);
212        let met = SameVmctxPartition::meet(&mut self.scratch_groups, current, partition);
213        self.partitions[module] = met;
214    }
215
216    /// Finish observing and produce the queryable analysis results.
217    fn finish(self) -> SameVmctxImports {
218        SameVmctxImports {
219            partitions: self.partitions,
220        }
221    }
222}
223
224/// A completed same-`vmctx` analysis, ready to be queried.
225struct SameVmctxImports {
226    partitions: SecondaryMap<StaticModuleIndex, SameVmctxPartition>,
227}
228
229impl SameVmctxImports {
230    /// The function import of `module` whose `vmctx` slot `func` should be
231    /// compiled to use.
232    fn representative(&self, module: StaticModuleIndex, func: FuncIndex) -> FuncIndex {
233        self.partitions[module].representative(func)
234    }
235}
236
237/// Run the same-`vmctx` analysis over `dfg`, recording its results in each
238/// module's `ModuleTranslation::same_vmctx_imported_functions`.
239pub fn analyze_same_vmctx_imports(
240    dfg: &ComponentDfg,
241    static_modules: &mut PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
242) {
243    let mut builder = SameVmctxBuilder::default();
244    let mut keys = VmctxKeys::new(static_modules);
245
246    // Scratch space, reused across the calls below.
247    let mut func_keys = Vec::new();
248    let mut stack = Vec::new();
249
250    // Resolve every core instance's arguments, in order.
251    for (id, instance) in dfg.instances.iter() {
252        keys.push(dfg, id, instance);
253    }
254
255    // Observe every core module instantiation that the component itself
256    // performs.
257    for effect in dfg.side_effects.iter() {
258        let SideEffect::Instance(id, _) = effect else {
259            continue;
260        };
261
262        let Instance::Static(module, _) = &dfg.instances[*id] else {
263            // A module imported from the host is not one we are compiling.
264            continue;
265        };
266
267        observe_instantiation(
268            &mut func_keys,
269            &mut builder,
270            static_modules,
271            *module,
272            keys.args[*id].iter().copied(),
273        );
274    }
275
276    // Adapter modules do not appear in `side_effects`; they are instantiated
277    // lazily, at most once each, as their adapters are referenced.
278    for (_, (module, args)) in dfg.adapter_modules.iter() {
279        observe_instantiation(
280            &mut func_keys,
281            &mut builder,
282            static_modules,
283            *module,
284            args.iter().map(|arg| keys.vmctx_key(dfg, arg)),
285        );
286    }
287
288    // The host may instantiate an exported module with anything at all.
289    for (_, (export, _)) in dfg.exports.iter() {
290        observe_exported_modules(&mut stack, &mut builder, export);
291    }
292
293    let analysis = builder.finish();
294
295    for (module, translation) in static_modules.iter_mut() {
296        for i in 0..translation.module.num_imported_funcs {
297            let func = FuncIndex::new(i);
298            let representative = analysis.representative(module, func);
299            if representative != func {
300                translation.imported_func_vmctx_representative[func] = representative.into();
301            }
302        }
303    }
304}
305
306/// Observe an instantiation of `module`, where `args` yields the `vmctx` key
307/// of each of its positional arguments.
308///
309/// `func_keys` is scratch space, reused across calls to avoid reallocating a
310/// vector per instantiation. Its contents on entry are ignored.
311fn observe_instantiation(
312    func_keys: &mut Vec<Option<VmctxKey>>,
313    builder: &mut SameVmctxBuilder,
314    static_modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
315    module: StaticModuleIndex,
316    args: impl Iterator<Item = Option<VmctxKey>>,
317) {
318    let translation = &static_modules[module];
319    func_keys.clear();
320    func_keys.resize(translation.module.num_imported_funcs, None);
321
322    for (position, key) in args.enumerate() {
323        let Some(EntityIndex::Function(func)) = translation.module.import_index(position) else {
324            continue;
325        };
326        func_keys[func.index()] = key;
327    }
328
329    builder.observe_instantiation(module, func_keys);
330}
331
332/// Observe every static module reachable from `export`, any of which the host
333/// may instantiate however it likes.
334///
335/// `stack` is scratch space, reused across calls to avoid reallocating a
336/// vector per export. Its contents on entry are ignored.
337fn observe_exported_modules<'a>(
338    stack: &mut Vec<&'a Export>,
339    builder: &mut SameVmctxBuilder,
340    export: &'a Export,
341) {
342    stack.clear();
343    stack.push(export);
344
345    while let Some(export) = stack.pop() {
346        match export {
347            Export::ModuleStatic { index, .. } => builder.observe_unknown_instantiation(*index),
348
349            Export::Instance { exports, .. } => {
350                stack.extend(exports.iter().map(|(_, (export, _))| export));
351            }
352
353            Export::LiftedFunction { .. } | Export::ModuleImport { .. } | Export::Type(_) => {}
354        }
355    }
356}
357
358/// The `vmctx` key of every core instance's arguments, memoized so that
359/// resolving a re-export takes a single step rather than walking the whole
360/// chain of re-exports back to the definition.
361struct VmctxKeys<'a> {
362    /// The `vmctx` key of each instance's arguments, by import position.
363    ///
364    /// Empty for instances of host modules, which have no import positions
365    /// that we can see into.
366    args: PrimaryMap<InstanceId, Vec<Option<VmctxKey>>>,
367
368    /// The static modules being instantiated.
369    static_modules: &'a PrimaryMap<StaticModuleIndex, ModuleTranslation<'a>>,
370
371    /// The position of each import of each static module.
372    import_positions: PrimaryMap<StaticModuleIndex, ImportPositions>,
373}
374
375impl<'a> VmctxKeys<'a> {
376    fn new(static_modules: &'a PrimaryMap<StaticModuleIndex, ModuleTranslation<'a>>) -> Self {
377        VmctxKeys {
378            args: PrimaryMap::new(),
379            static_modules,
380            import_positions: static_modules
381                .values()
382                .map(|translation| translation.module.import_positions())
383                .collect(),
384        }
385    }
386
387    /// Resolve the arguments of the next instance, `id`.
388    fn push(&mut self, dfg: &ComponentDfg, id: InstanceId, instance: &Instance) {
389        let keys = match instance {
390            Instance::Static(_, args) => args.iter().map(|arg| self.vmctx_key(dfg, arg)).collect(),
391            Instance::Import(..) => Vec::new(),
392        };
393        let pushed = self.args.push(keys);
394        debug_assert_eq!(pushed, id);
395    }
396
397    /// The `vmctx` that `def`'s `VMFuncRef` carries, when we can see it
398    /// statically.
399    fn vmctx_key(&self, dfg: &ComponentDfg, def: &CoreDef) -> Option<VmctxKey> {
400        // NB: deliberately exhaustive so that new variants must be classified
401        // here.
402        let export = match def {
403            // Trampolines and unsafe intrinsics take their `VMFuncRef` from
404            // the component instance, so they all share its context.
405            CoreDef::Trampoline(_) | CoreDef::UnsafeIntrinsic(..) => {
406                return Some(VmctxKey::Component);
407            }
408
409            CoreDef::Adapter(id) => return adapter_vmctx_key(dfg, self.static_modules, *id),
410
411            // Not a function, so it never satisfies a function import. Be
412            // conservative anyway.
413            CoreDef::InstanceFlags(_) => return None,
414
415            CoreDef::Export(export) => export,
416        };
417
418        let Instance::Static(module, _) = &dfg.instances[export.instance] else {
419            // An instance of a host module, whose exports we cannot see into.
420            return None;
421        };
422
423        let ExportItem::Index(index) = &export.item else {
424            // Names are only used for instances of modules whose shape is not
425            // statically known, which the arm above filtered out.
426            unreachable!()
427        };
428
429        // The common case: this instance's module defines the function, so
430        // the function's context is this instance's context.
431        let Some(position) = self.import_positions[*module].get(*index) else {
432            return Some(VmctxKey::CoreInstance(export.instance));
433        };
434
435        // Otherwise it is a re-export of one of the module's imports, so it has
436        // the same context as whichever argument satisfied that import.
437        *self.args.get(export.instance)?.get(position)?
438    }
439}
440
441/// The `vmctx` of a fused adapter, which lives in the instance of the adapter
442/// module it was compiled into.
443fn adapter_vmctx_key(
444    dfg: &ComponentDfg,
445    static_modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
446    id: AdapterId,
447) -> Option<VmctxKey> {
448    let (adapter_module, index) = *dfg.adapter_partitionings.get(id)?;
449    let (static_module, _) = dfg.adapter_modules[adapter_module];
450
451    debug_assert!(
452        !static_modules[static_module].module.is_imported(index),
453        "adapter modules always define their exported adapters",
454    );
455
456    Some(VmctxKey::AdapterModule(adapter_module))
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::property_check;
463    use mutatis::{Mutate, check::CheckResult, mutators as m};
464
465    impl SameVmctxPartition {
466        /// Are `a` and `b` in the same block of this partition?
467        fn same_block(&self, a: FuncIndex, b: FuncIndex) -> bool {
468            // A block's representative is a canonical name for it.
469            self.representative(a) == self.representative(b)
470        }
471
472        /// This partition's blocks over the function imports `0..n`, in canonical
473        /// form: each block is sorted, and the blocks are ordered by their least
474        /// member.
475        fn blocks(&self, n: u32) -> Vec<Vec<u32>> {
476            let mut blocks = std::collections::BTreeMap::<u32, Vec<u32>>::new();
477            for i in 0..n {
478                let rep = self.representative(FuncIndex::from_u32(i)).as_u32();
479                blocks.entry(rep).or_default().push(i);
480            }
481            blocks.into_values().collect()
482        }
483
484        /// Is `self <= other` in this lattice, i.e. is every block of `self`
485        /// contained within a block of `other`?
486        fn refines(&self, other: &Self, n: u32) -> bool {
487            (0..n).all(|i| {
488                (0..n).all(|j| {
489                    let (a, b) = (FuncIndex::from_u32(i), FuncIndex::from_u32(j));
490                    !self.same_block(a, b) || other.same_block(a, b)
491                })
492            })
493        }
494    }
495
496    /// A `vmctx` key naming the `i`th core instance.
497    fn key(i: u32) -> Option<VmctxKey> {
498        Some(VmctxKey::CoreInstance(InstanceId::from_u32(i)))
499    }
500
501    /// The absence of a key, i.e. an import whose `vmctx` we cannot determine.
502    fn unknown() -> Option<VmctxKey> {
503        None
504    }
505
506    /// Partition one module's imports, given one entry per instantiation
507    /// holding the `vmctx` key of each of the module's function imports.
508    fn analyze(instantiations: &[&[Option<VmctxKey>]]) -> SameVmctxPartition {
509        let module = StaticModuleIndex::from_u32(0);
510        let mut builder = SameVmctxBuilder::default();
511        for keys in instantiations {
512            builder.observe_instantiation(module, keys);
513        }
514        builder.finish().partitions[module].clone()
515    }
516
517    /// Like `analyze`, but returning the canonical blocks over `n` function
518    /// imports, which is what most assertions below are about.
519    fn blocks(n: u32, instantiations: &[&[Option<VmctxKey>]]) -> Vec<Vec<u32>> {
520        analyze(instantiations).blocks(n)
521    }
522
523    #[test]
524    fn a_module_that_is_never_instantiated_stays_at_top() {
525        // Vacuously, every import shares a `vmctx` with every other: there
526        // is no instantiation to say otherwise.
527        let p = analyze(&[]);
528        assert!(matches!(p, SameVmctxPartition::Top));
529        assert_eq!(p.blocks(4), vec![vec![0, 1, 2, 3]]);
530
531        for i in 0..4 {
532            assert_eq!(
533                p.representative(FuncIndex::from_u32(i)),
534                FuncIndex::from_u32(0),
535                "import {i} should use import 0's `vmctx` slot",
536            );
537        }
538    }
539
540    #[test]
541    fn one_instantiation_with_all_imports_from_one_instance() {
542        assert_eq!(
543            blocks(4, &[&[key(0), key(0), key(0), key(0)]]),
544            vec![vec![0, 1, 2, 3]],
545        );
546    }
547
548    #[test]
549    fn one_instantiation_with_all_imports_from_distinct_instances() {
550        assert_eq!(
551            blocks(4, &[&[key(0), key(1), key(2), key(3)]]),
552            vec![vec![0], vec![1], vec![2], vec![3]],
553        );
554    }
555
556    #[test]
557    fn one_instantiation_split_across_two_instances() {
558        assert_eq!(
559            blocks(4, &[&[key(9), key(9), key(5), key(5)]]),
560            vec![vec![0, 1], vec![2, 3]],
561        );
562    }
563
564    #[test]
565    fn one_instantiation_with_interleaved_instances() {
566        // Blocks need not be contiguous ranges of indices.
567        assert_eq!(
568            blocks(4, &[&[key(0), key(1), key(0), key(1)]]),
569            vec![vec![0, 2], vec![1, 3]],
570        );
571    }
572
573    #[test]
574    fn unknown_keys_are_singletons() {
575        // An unresolved import shares a `vmctx` with nothing, not even
576        // another unresolved import.
577        assert_eq!(
578            blocks(5, &[&[key(0), unknown(), key(0), unknown(), key(0)]]),
579            vec![vec![0, 2, 4], vec![1], vec![3]],
580        );
581    }
582
583    #[test]
584    fn distinct_key_variants_are_distinct_keys() {
585        // These three keys all carry a `0`-ish payload but name three
586        // different contexts.
587        let component = Some(VmctxKey::Component);
588        let instance = Some(VmctxKey::CoreInstance(InstanceId::from_u32(0)));
589        let adapter = Some(VmctxKey::AdapterModule(AdapterModuleId::from_u32(0)));
590        assert_eq!(
591            blocks(
592                6,
593                &[&[component, instance, adapter, component, instance, adapter]]
594            ),
595            vec![vec![0, 3], vec![1, 4], vec![2, 5]],
596        );
597    }
598
599    #[test]
600    fn two_agreeing_instantiations_change_nothing() {
601        // Different instances, but the same *partition*.
602        assert_eq!(
603            blocks(
604                4,
605                &[
606                    &[key(0), key(0), key(1), key(1)],
607                    &[key(7), key(7), key(8), key(8)],
608                ],
609            ),
610            vec![vec![0, 1], vec![2, 3]],
611        );
612    }
613
614    #[test]
615    fn a_disagreeing_instantiation_breaks_everything_apart() {
616        assert_eq!(
617            blocks(
618                4,
619                &[
620                    &[key(0), key(0), key(0), key(0)],
621                    &[key(0), key(1), key(2), key(3)],
622                ],
623            ),
624            vec![vec![0], vec![1], vec![2], vec![3]],
625        );
626    }
627
628    #[test]
629    fn a_partially_disagreeing_instantiation_keeps_what_it_agrees_on() {
630        assert_eq!(
631            blocks(
632                4,
633                &[
634                    &[key(0), key(0), key(0), key(0)],
635                    &[key(0), key(0), key(1), key(1)],
636                ],
637            ),
638            vec![vec![0, 1], vec![2, 3]],
639        );
640    }
641
642    #[test]
643    fn crossing_splits_meet_to_all_singletons() {
644        // `{0,1}{2,3}` and `{0,2}{1,3}` share no pair, so nothing survives
645        // even though neither operand is near bottom.
646        assert_eq!(
647            blocks(
648                4,
649                &[
650                    &[key(0), key(0), key(1), key(1)],
651                    &[key(0), key(1), key(0), key(1)],
652                ],
653            ),
654            vec![vec![0], vec![1], vec![2], vec![3]],
655        );
656    }
657
658    #[test]
659    fn partially_crossing_splits_keep_their_common_refinement() {
660        // `{0,1,2}{3,4,5}` meet `{0,1}{2,3}{4,5}` == `{0,1}{2}{3}{4,5}`.
661        assert_eq!(
662            blocks(
663                6,
664                &[
665                    &[key(0), key(0), key(0), key(1), key(1), key(1)],
666                    &[key(0), key(0), key(1), key(1), key(2), key(2)],
667                ],
668            ),
669            vec![vec![0, 1], vec![2], vec![3], vec![4, 5]],
670        );
671    }
672
673    #[test]
674    fn progressive_refinement_is_order_independent() {
675        let a: &[Option<VmctxKey>] = &[key(0), key(0), key(0), key(0), key(0), key(0)];
676        let b: &[Option<VmctxKey>] = &[key(0), key(0), key(0), key(1), key(1), key(1)];
677        let c: &[Option<VmctxKey>] = &[key(0), key(0), key(1), key(1), key(2), key(2)];
678
679        let expected = vec![vec![0, 1], vec![2], vec![3], vec![4, 5]];
680        for order in [
681            [a, b, c],
682            [a, c, b],
683            [b, a, c],
684            [b, c, a],
685            [c, a, b],
686            [c, b, a],
687        ] {
688            assert_eq!(blocks(6, &order), expected, "order {order:?} disagreed");
689        }
690    }
691
692    #[test]
693    fn an_unknown_instantiation_forces_bottom_and_cannot_be_undone() {
694        let module = StaticModuleIndex::from_u32(0);
695        let mut builder = SameVmctxBuilder::default();
696
697        builder.observe_instantiation(module, &[key(0), key(0), key(0)]);
698        builder.observe_unknown_instantiation(module);
699        // Nothing observed later can coarsen the partition back up.
700        builder.observe_instantiation(module, &[key(0), key(0), key(0)]);
701
702        let p = builder.finish().partitions[module].clone();
703        assert_eq!(p.blocks(3), SameVmctxPartition::bottom().blocks(3));
704    }
705
706    #[test]
707    fn modules_do_not_interfere_with_each_other() {
708        let a = StaticModuleIndex::from_u32(0);
709        let b = StaticModuleIndex::from_u32(3);
710        let mut builder = SameVmctxBuilder::default();
711
712        builder.observe_instantiation(a, &[key(0), key(0)]);
713        builder.observe_instantiation(b, &[key(0), key(1)]);
714
715        let analysis = builder.finish();
716        assert_eq!(analysis.partitions[a].blocks(2), vec![vec![0, 1]]);
717        assert_eq!(analysis.partitions[b].blocks(2), vec![vec![0], vec![1]]);
718        // An untouched module in between is still at top.
719        assert_eq!(
720            analysis.partitions[StaticModuleIndex::from_u32(1)].blocks(2),
721            vec![vec![0, 1]],
722        );
723    }
724
725    #[test]
726    fn degenerate_numbers_of_imports() {
727        // Zero function imports: nothing to partition, and nothing that
728        // could ask for a representative.
729        assert_eq!(blocks(0, &[]), Vec::<Vec<u32>>::new());
730        assert_eq!(blocks(0, &[&[]]), Vec::<Vec<u32>>::new());
731
732        // One function import: it shares a `vmctx` with itself no matter
733        // what we observe.
734        assert_eq!(blocks(1, &[]), vec![vec![0]]);
735        assert_eq!(blocks(1, &[&[key(0)]]), vec![vec![0]]);
736        assert_eq!(blocks(1, &[&[unknown()]]), vec![vec![0]]);
737        assert_eq!(blocks(1, &[&[key(0)], &[key(1)]]), vec![vec![0]]);
738    }
739
740    #[test]
741    fn a_representative_is_always_its_blocks_least_member() {
742        let p = analyze(&[&[key(1), key(0), key(1), key(0), key(1)]]);
743        let rep = |i| p.representative(FuncIndex::from_u32(i)).as_u32();
744        assert_eq!(rep(0), 0);
745        assert_eq!(rep(2), 0);
746        assert_eq!(rep(4), 0);
747        assert_eq!(rep(1), 1);
748        assert_eq!(rep(3), 1);
749
750        // Even when import 0 is in no block but its own.
751        let p = analyze(&[&[unknown(), key(0), key(1), key(0), key(1)]]);
752        let rep = |i| p.representative(FuncIndex::from_u32(i)).as_u32();
753        assert_eq!(rep(0), 0);
754        assert_eq!(rep(1), 1);
755        assert_eq!(rep(3), 1);
756        assert_eq!(rep(2), 2);
757        assert_eq!(rep(4), 2);
758    }
759
760    #[test]
761    fn many_imports_in_many_blocks() {
762        // Exercise the union-find with more than the handful of imports the
763        // tests above use: 64 imports in 8 strided blocks of 8.
764        let keys = (0..64).map(|i| key(i % 8)).collect::<Vec<_>>();
765        let expected = (0..8)
766            .map(|b| (0..8).map(|i| b + i * 8).collect::<Vec<u32>>())
767            .collect::<Vec<_>>();
768        assert_eq!(blocks(64, &[&keys]), expected);
769
770        // Meeting that with 8 *contiguous* blocks of 8 leaves all
771        // singletons: each contiguous block shares exactly one member with
772        // each strided one.
773        let contiguous = (0..64).map(|i| key(i / 8)).collect::<Vec<_>>();
774        assert_eq!(
775            blocks(64, &[&keys, &contiguous]),
776            (0..64).map(|i| vec![i]).collect::<Vec<_>>(),
777        );
778    }
779
780    // Lattice laws.
781
782    /// The number of function imports the lattice-law tests partition.
783    const N: u32 = 6;
784
785    /// Build a partition of `0..N` from block labels, where `None` is an
786    /// unknown key, i.e. a forced singleton.
787    fn partition(labels: &[Option<u32>]) -> SameVmctxPartition {
788        let keys = labels
789            .iter()
790            .map(|l| match l {
791                Some(l) => key(*l),
792                None => unknown(),
793            })
794            .collect::<Vec<_>>();
795        SameVmctxPartition::from_keys(&mut HashMap::new(), &keys)
796    }
797
798    /// Decode a chunk of random bytes into a partition of `0..N`.
799    ///
800    /// The first byte chooses between the two extremal elements and a
801    /// partition built from the remaining bytes, one label per function
802    /// import, so that top and bottom come up often enough to exercise the
803    /// identity and absorption laws.
804    fn decode(bytes: &[u8]) -> SameVmctxPartition {
805        let byte = |i: usize| bytes.get(i).copied().unwrap_or(0);
806        match byte(0) % 8 {
807            0 => SameVmctxPartition::Top,
808            1 => SameVmctxPartition::bottom(),
809            _ => {
810                let labels = (0..N)
811                    .map(|i| match byte(1 + i as usize) % 5 {
812                        // One label in five is "unknown".
813                        4 => None,
814                        l => Some(u32::from(l)),
815                    })
816                    .collect::<Vec<_>>();
817                partition(&labels)
818            }
819        }
820    }
821
822    /// Split a byte string into the three partitions the laws are checked on.
823    fn decode3(bytes: &[u8]) -> (SameVmctxPartition, SameVmctxPartition, SameVmctxPartition) {
824        let chunk = 1 + N as usize;
825        let at = |i: usize| decode(bytes.get(i * chunk..).unwrap_or(&[]));
826        (at(0), at(1), at(2))
827    }
828
829    /// `SameVmctxPartition::meet` on borrowed operands: the laws below apply
830    /// it repeatedly to the same partitions, but it consumes them.
831    fn meet(a: &SameVmctxPartition, b: &SameVmctxPartition) -> SameVmctxPartition {
832        SameVmctxPartition::meet(&mut HashMap::new(), a.clone(), b.clone())
833    }
834
835    /// An obviously-correct meet to cross-check the real one against: two
836    /// imports share a block of the result exactly when they share a block of
837    /// both operands.
838    fn reference_meet(a: &SameVmctxPartition, b: &SameVmctxPartition) -> Vec<Vec<u32>> {
839        let mut assigned = vec![false; N as usize];
840        let mut blocks = Vec::new();
841        for i in 0..N {
842            if assigned[i as usize] {
843                continue;
844            }
845            let mut block = Vec::new();
846            for j in i..N {
847                let (x, y) = (FuncIndex::from_u32(i), FuncIndex::from_u32(j));
848                if !assigned[j as usize] && a.same_block(x, y) && b.same_block(x, y) {
849                    assigned[j as usize] = true;
850                    block.push(j);
851                }
852            }
853            blocks.push(block);
854        }
855        blocks
856    }
857
858    #[test]
859    fn top_and_bottom_are_the_lattice_bounds() {
860        let top = SameVmctxPartition::Top;
861        let bottom = SameVmctxPartition::bottom();
862        let p = partition(&[Some(0), Some(0), Some(1), Some(1), None, None]);
863
864        assert_eq!(meet(&top, &p).blocks(N), p.blocks(N));
865        assert_eq!(meet(&p, &top).blocks(N), p.blocks(N));
866        assert_eq!(meet(&bottom, &p).blocks(N), bottom.blocks(N));
867        assert_eq!(meet(&p, &bottom).blocks(N), bottom.blocks(N));
868
869        assert!(p.refines(&top, N));
870        assert!(bottom.refines(&p, N));
871        assert!(!top.refines(&p, N));
872        assert!(!p.refines(&bottom, N));
873    }
874
875    #[test]
876    fn meet_is_idempotent() {
877        for p in [
878            SameVmctxPartition::Top,
879            SameVmctxPartition::bottom(),
880            partition(&[Some(0), Some(0), Some(1), Some(1), Some(2), None]),
881            partition(&[Some(3), Some(3), Some(3), Some(3), Some(3), Some(3)]),
882        ] {
883            assert_eq!(meet(&p, &p).blocks(N), p.blocks(N));
884        }
885    }
886
887    #[test]
888    fn meet_laws_hold_on_random_partitions() -> CheckResult<Vec<u8>> {
889        let mutator = m::default::<Vec<u8>>().map(|_ctx, bytes| {
890            bytes.truncate(3 * (1 + N as usize));
891            Ok(())
892        });
893
894        property_check().run_with(mutator, [Vec::new()], |bytes| {
895            let (a, b, c) = decode3(bytes);
896
897            // Commutative.
898            let ab = meet(&a, &b);
899            let ba = meet(&b, &a);
900            assert_eq!(ab.blocks(N), ba.blocks(N), "meet is not commutative");
901
902            // Associative.
903            let ab_c = meet(&ab, &c);
904            let a_bc = meet(&a, &meet(&b, &c));
905            assert_eq!(ab_c.blocks(N), a_bc.blocks(N), "meet is not associative");
906
907            // Idempotent.
908            assert_eq!(
909                meet(&a, &a).blocks(N),
910                a.blocks(N),
911                "meet is not idempotent"
912            );
913
914            // Agrees with the naive definition.
915            assert_eq!(
916                ab.blocks(N),
917                reference_meet(&a, &b),
918                "meet disagrees with the reference meet",
919            );
920
921            // A lower bound of both operands.
922            assert!(ab.refines(&a, N), "meet does not refine its left operand");
923            assert!(ab.refines(&b, N), "meet does not refine its right operand");
924
925            // And the *greatest* one: anything below both is below it.
926            if c.refines(&a, N) && c.refines(&b, N) {
927                assert!(
928                    c.refines(&ab, N),
929                    "meet is not the greatest lower bound of its operands",
930                );
931            }
932
933            // Monotone: `ab <= a`, so meeting both with `c` preserves that
934            // ordering.
935            assert!(
936                meet(&ab, &c).refines(&meet(&a, &c), N),
937                "meet is not monotone in its left argument",
938            );
939            assert!(
940                meet(&c, &ab).refines(&meet(&c, &a), N),
941                "meet is not monotone in its right argument",
942            );
943
944            // Refinement and meet define the same order.
945            assert_eq!(
946                a.refines(&b, N),
947                ab.blocks(N) == a.blocks(N),
948                "`a <= b` and `a /\\ b == a` disagree",
949            );
950
951            Ok::<_, String>(())
952        })
953    }
954
955    #[test]
956    fn representatives_are_least_members_on_random_partitions() -> CheckResult<Vec<u8>> {
957        let mutator = m::default::<Vec<u8>>().map(|_ctx, bytes| {
958            bytes.truncate(1 + N as usize);
959            Ok(())
960        });
961
962        property_check().run_with(mutator, [Vec::new()], |bytes| {
963            let p = decode(bytes);
964            for block in p.blocks(N) {
965                let least = *block.first().unwrap();
966                for i in block {
967                    assert_eq!(
968                        p.representative(FuncIndex::from_u32(i)).as_u32(),
969                        least,
970                        "every member of a block must name the same representative",
971                    );
972                }
973            }
974            Ok::<_, String>(())
975        })
976    }
977}