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