use crate::compile::ModuleTranslation;
use crate::component::ExportItem;
use crate::component::dfg::{
AdapterId, AdapterModuleId, ComponentDfg, CoreDef, Export, Instance, InstanceId, SideEffect,
};
use crate::prelude::*;
use crate::union_find::UnionFind;
use crate::{EntityIndex, EntityRef, FuncIndex, PrimaryMap, SecondaryMap, StaticModuleIndex};
use core::mem;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum VmctxKey {
Component,
CoreInstance(InstanceId),
AdapterModule(AdapterModuleId),
}
#[derive(Clone, Debug, Default)]
enum SameVmctxPartition {
#[default]
Top,
Known(UnionFind<FuncIndex>),
}
impl SameVmctxPartition {
fn bottom() -> Self {
SameVmctxPartition::Known(UnionFind::new())
}
fn from_keys(first: &mut HashMap<VmctxKey, FuncIndex>, keys: &[Option<VmctxKey>]) -> Self {
first.clear();
let mut sets = UnionFind::new();
for (i, key) in keys.iter().enumerate() {
let Some(key) = *key else { continue };
let func = FuncIndex::new(i);
match first.entry(key) {
Entry::Occupied(e) => {
sets.union(*e.get(), func);
}
Entry::Vacant(e) => {
e.insert(func);
}
}
}
SameVmctxPartition::Known(sets)
}
fn meet(groups: &mut HashMap<(FuncIndex, FuncIndex), FuncIndex>, a: Self, b: Self) -> Self {
use SameVmctxPartition::*;
match (a, b) {
(Top, p) | (p, Top) => p,
(Known(ref a), Known(ref b)) => {
let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
let mut sets = UnionFind::new();
groups.clear();
for func in small.elems() {
if !large.contains(func) {
continue;
}
let group = (
a.find_without_path_compression(func),
b.find_without_path_compression(func),
);
match groups.entry(group) {
Entry::Occupied(e) => {
sets.union(*e.get(), func);
}
Entry::Vacant(e) => {
e.insert(func);
}
}
}
Known(sets)
}
}
}
fn representative(&self, func: FuncIndex) -> FuncIndex {
match self {
SameVmctxPartition::Top => FuncIndex::new(0),
SameVmctxPartition::Known(sets) => sets.set_min(func),
}
}
}
#[derive(Default)]
struct SameVmctxBuilder {
partitions: SecondaryMap<StaticModuleIndex, SameVmctxPartition>,
scratch_first: HashMap<VmctxKey, FuncIndex>,
scratch_groups: HashMap<(FuncIndex, FuncIndex), FuncIndex>,
}
impl SameVmctxBuilder {
fn observe_instantiation(&mut self, module: StaticModuleIndex, keys: &[Option<VmctxKey>]) {
let partition = SameVmctxPartition::from_keys(&mut self.scratch_first, keys);
self.observe(module, partition);
}
fn observe_unknown_instantiation(&mut self, module: StaticModuleIndex) {
self.observe(module, SameVmctxPartition::bottom());
}
fn observe(&mut self, module: StaticModuleIndex, partition: SameVmctxPartition) {
let current = mem::take(&mut self.partitions[module]);
let met = SameVmctxPartition::meet(&mut self.scratch_groups, current, partition);
self.partitions[module] = met;
}
fn finish(self) -> SameVmctxImports {
SameVmctxImports {
partitions: self.partitions,
}
}
}
struct SameVmctxImports {
partitions: SecondaryMap<StaticModuleIndex, SameVmctxPartition>,
}
impl SameVmctxImports {
fn representative(&self, module: StaticModuleIndex, func: FuncIndex) -> FuncIndex {
self.partitions[module].representative(func)
}
}
pub fn analyze_same_vmctx_imports(
dfg: &ComponentDfg,
static_modules: &mut PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
) {
let mut builder = SameVmctxBuilder::default();
let mut keys = Vec::new();
let mut stack = Vec::new();
for effect in dfg.side_effects.iter() {
let SideEffect::Instance(id, _) = effect else {
continue;
};
let Instance::Static(module, args) = &dfg.instances[*id] else {
continue;
};
observe_instantiation(&mut keys, &mut builder, dfg, static_modules, *module, args);
}
for (_, (module, args)) in dfg.adapter_modules.iter() {
observe_instantiation(&mut keys, &mut builder, dfg, static_modules, *module, args);
}
for (_, (export, _)) in dfg.exports.iter() {
observe_exported_modules(&mut stack, &mut builder, export);
}
let analysis = builder.finish();
for (module, translation) in static_modules.iter_mut() {
for i in 0..translation.module.num_imported_funcs {
let func = FuncIndex::new(i);
let representative = analysis.representative(module, func);
if representative != func {
translation.imported_func_vmctx_representative[func] = representative.into();
}
}
}
}
fn observe_instantiation(
keys: &mut Vec<Option<VmctxKey>>,
builder: &mut SameVmctxBuilder,
dfg: &ComponentDfg,
static_modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
module: StaticModuleIndex,
args: &[CoreDef],
) {
let translation = &static_modules[module];
keys.clear();
keys.resize(translation.module.num_imported_funcs, None);
for (position, arg) in args.iter().enumerate() {
let Some(EntityIndex::Function(func)) = translation.module.import_index(position) else {
continue;
};
keys[func.index()] = vmctx_key(dfg, static_modules, arg);
}
builder.observe_instantiation(module, keys);
}
fn observe_exported_modules<'a>(
stack: &mut Vec<&'a Export>,
builder: &mut SameVmctxBuilder,
export: &'a Export,
) {
stack.clear();
stack.push(export);
while let Some(export) = stack.pop() {
match export {
Export::ModuleStatic { index, .. } => builder.observe_unknown_instantiation(*index),
Export::Instance { exports, .. } => {
stack.extend(exports.iter().map(|(_, (export, _))| export));
}
Export::LiftedFunction { .. } | Export::ModuleImport { .. } | Export::Type(_) => {}
}
}
}
fn vmctx_key(
dfg: &ComponentDfg,
static_modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
def: &CoreDef,
) -> Option<VmctxKey> {
let mut def = def;
let mut previous: Option<InstanceId> = None;
loop {
let export = match def {
CoreDef::Trampoline(_) | CoreDef::UnsafeIntrinsic(..) => {
return Some(VmctxKey::Component);
}
CoreDef::Adapter(id) => return adapter_vmctx_key(dfg, static_modules, *id),
CoreDef::InstanceFlags(_) => return None,
CoreDef::Export(export) => export,
};
if previous.is_some_and(|p| export.instance.index() >= p.index()) {
return None;
}
previous = Some(export.instance);
let Instance::Static(module, args) = &dfg.instances[export.instance] else {
return None;
};
let ExportItem::Index(index) = &export.item else {
return None;
};
let module = &static_modules[*module].module;
if !module.is_imported(*index) {
return Some(VmctxKey::CoreInstance(export.instance));
}
let position = module
.import_position(*index)
.expect("imported entities always have an associated import initializer");
def = &args[position];
}
}
fn adapter_vmctx_key(
dfg: &ComponentDfg,
static_modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
id: AdapterId,
) -> Option<VmctxKey> {
let (adapter_module, index) = *dfg.adapter_partitionings.get(id)?;
let (static_module, _) = dfg.adapter_modules[adapter_module];
debug_assert!(
!static_modules[static_module].module.is_imported(index),
"adapter modules always define their exported adapters",
);
Some(VmctxKey::AdapterModule(adapter_module))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property_check;
use mutatis::{Mutate, check::CheckResult, mutators as m};
impl SameVmctxPartition {
fn same_block(&self, a: FuncIndex, b: FuncIndex) -> bool {
self.representative(a) == self.representative(b)
}
fn blocks(&self, n: u32) -> Vec<Vec<u32>> {
let mut blocks = std::collections::BTreeMap::<u32, Vec<u32>>::new();
for i in 0..n {
let rep = self.representative(FuncIndex::from_u32(i)).as_u32();
blocks.entry(rep).or_default().push(i);
}
blocks.into_values().collect()
}
fn refines(&self, other: &Self, n: u32) -> bool {
(0..n).all(|i| {
(0..n).all(|j| {
let (a, b) = (FuncIndex::from_u32(i), FuncIndex::from_u32(j));
!self.same_block(a, b) || other.same_block(a, b)
})
})
}
}
fn key(i: u32) -> Option<VmctxKey> {
Some(VmctxKey::CoreInstance(InstanceId::from_u32(i)))
}
fn unknown() -> Option<VmctxKey> {
None
}
fn analyze(instantiations: &[&[Option<VmctxKey>]]) -> SameVmctxPartition {
let module = StaticModuleIndex::from_u32(0);
let mut builder = SameVmctxBuilder::default();
for keys in instantiations {
builder.observe_instantiation(module, keys);
}
builder.finish().partitions[module].clone()
}
fn blocks(n: u32, instantiations: &[&[Option<VmctxKey>]]) -> Vec<Vec<u32>> {
analyze(instantiations).blocks(n)
}
#[test]
fn a_module_that_is_never_instantiated_stays_at_top() {
let p = analyze(&[]);
assert!(matches!(p, SameVmctxPartition::Top));
assert_eq!(p.blocks(4), vec![vec![0, 1, 2, 3]]);
for i in 0..4 {
assert_eq!(
p.representative(FuncIndex::from_u32(i)),
FuncIndex::from_u32(0),
"import {i} should use import 0's `vmctx` slot",
);
}
}
#[test]
fn one_instantiation_with_all_imports_from_one_instance() {
assert_eq!(
blocks(4, &[&[key(0), key(0), key(0), key(0)]]),
vec![vec![0, 1, 2, 3]],
);
}
#[test]
fn one_instantiation_with_all_imports_from_distinct_instances() {
assert_eq!(
blocks(4, &[&[key(0), key(1), key(2), key(3)]]),
vec![vec![0], vec![1], vec![2], vec![3]],
);
}
#[test]
fn one_instantiation_split_across_two_instances() {
assert_eq!(
blocks(4, &[&[key(9), key(9), key(5), key(5)]]),
vec![vec![0, 1], vec![2, 3]],
);
}
#[test]
fn one_instantiation_with_interleaved_instances() {
assert_eq!(
blocks(4, &[&[key(0), key(1), key(0), key(1)]]),
vec![vec![0, 2], vec![1, 3]],
);
}
#[test]
fn unknown_keys_are_singletons() {
assert_eq!(
blocks(5, &[&[key(0), unknown(), key(0), unknown(), key(0)]]),
vec![vec![0, 2, 4], vec![1], vec![3]],
);
}
#[test]
fn distinct_key_variants_are_distinct_keys() {
let component = Some(VmctxKey::Component);
let instance = Some(VmctxKey::CoreInstance(InstanceId::from_u32(0)));
let adapter = Some(VmctxKey::AdapterModule(AdapterModuleId::from_u32(0)));
assert_eq!(
blocks(
6,
&[&[component, instance, adapter, component, instance, adapter]]
),
vec![vec![0, 3], vec![1, 4], vec![2, 5]],
);
}
#[test]
fn two_agreeing_instantiations_change_nothing() {
assert_eq!(
blocks(
4,
&[
&[key(0), key(0), key(1), key(1)],
&[key(7), key(7), key(8), key(8)],
],
),
vec![vec![0, 1], vec![2, 3]],
);
}
#[test]
fn a_disagreeing_instantiation_breaks_everything_apart() {
assert_eq!(
blocks(
4,
&[
&[key(0), key(0), key(0), key(0)],
&[key(0), key(1), key(2), key(3)],
],
),
vec![vec![0], vec![1], vec![2], vec![3]],
);
}
#[test]
fn a_partially_disagreeing_instantiation_keeps_what_it_agrees_on() {
assert_eq!(
blocks(
4,
&[
&[key(0), key(0), key(0), key(0)],
&[key(0), key(0), key(1), key(1)],
],
),
vec![vec![0, 1], vec![2, 3]],
);
}
#[test]
fn crossing_splits_meet_to_all_singletons() {
assert_eq!(
blocks(
4,
&[
&[key(0), key(0), key(1), key(1)],
&[key(0), key(1), key(0), key(1)],
],
),
vec![vec![0], vec![1], vec![2], vec![3]],
);
}
#[test]
fn partially_crossing_splits_keep_their_common_refinement() {
assert_eq!(
blocks(
6,
&[
&[key(0), key(0), key(0), key(1), key(1), key(1)],
&[key(0), key(0), key(1), key(1), key(2), key(2)],
],
),
vec![vec![0, 1], vec![2], vec![3], vec![4, 5]],
);
}
#[test]
fn progressive_refinement_is_order_independent() {
let a: &[Option<VmctxKey>] = &[key(0), key(0), key(0), key(0), key(0), key(0)];
let b: &[Option<VmctxKey>] = &[key(0), key(0), key(0), key(1), key(1), key(1)];
let c: &[Option<VmctxKey>] = &[key(0), key(0), key(1), key(1), key(2), key(2)];
let expected = vec![vec![0, 1], vec![2], vec![3], vec![4, 5]];
for order in [
[a, b, c],
[a, c, b],
[b, a, c],
[b, c, a],
[c, a, b],
[c, b, a],
] {
assert_eq!(blocks(6, &order), expected, "order {order:?} disagreed");
}
}
#[test]
fn an_unknown_instantiation_forces_bottom_and_cannot_be_undone() {
let module = StaticModuleIndex::from_u32(0);
let mut builder = SameVmctxBuilder::default();
builder.observe_instantiation(module, &[key(0), key(0), key(0)]);
builder.observe_unknown_instantiation(module);
builder.observe_instantiation(module, &[key(0), key(0), key(0)]);
let p = builder.finish().partitions[module].clone();
assert_eq!(p.blocks(3), SameVmctxPartition::bottom().blocks(3));
}
#[test]
fn modules_do_not_interfere_with_each_other() {
let a = StaticModuleIndex::from_u32(0);
let b = StaticModuleIndex::from_u32(3);
let mut builder = SameVmctxBuilder::default();
builder.observe_instantiation(a, &[key(0), key(0)]);
builder.observe_instantiation(b, &[key(0), key(1)]);
let analysis = builder.finish();
assert_eq!(analysis.partitions[a].blocks(2), vec![vec![0, 1]]);
assert_eq!(analysis.partitions[b].blocks(2), vec![vec![0], vec![1]]);
assert_eq!(
analysis.partitions[StaticModuleIndex::from_u32(1)].blocks(2),
vec![vec![0, 1]],
);
}
#[test]
fn degenerate_numbers_of_imports() {
assert_eq!(blocks(0, &[]), Vec::<Vec<u32>>::new());
assert_eq!(blocks(0, &[&[]]), Vec::<Vec<u32>>::new());
assert_eq!(blocks(1, &[]), vec![vec![0]]);
assert_eq!(blocks(1, &[&[key(0)]]), vec![vec![0]]);
assert_eq!(blocks(1, &[&[unknown()]]), vec![vec![0]]);
assert_eq!(blocks(1, &[&[key(0)], &[key(1)]]), vec![vec![0]]);
}
#[test]
fn a_representative_is_always_its_blocks_least_member() {
let p = analyze(&[&[key(1), key(0), key(1), key(0), key(1)]]);
let rep = |i| p.representative(FuncIndex::from_u32(i)).as_u32();
assert_eq!(rep(0), 0);
assert_eq!(rep(2), 0);
assert_eq!(rep(4), 0);
assert_eq!(rep(1), 1);
assert_eq!(rep(3), 1);
let p = analyze(&[&[unknown(), key(0), key(1), key(0), key(1)]]);
let rep = |i| p.representative(FuncIndex::from_u32(i)).as_u32();
assert_eq!(rep(0), 0);
assert_eq!(rep(1), 1);
assert_eq!(rep(3), 1);
assert_eq!(rep(2), 2);
assert_eq!(rep(4), 2);
}
#[test]
fn many_imports_in_many_blocks() {
let keys = (0..64).map(|i| key(i % 8)).collect::<Vec<_>>();
let expected = (0..8)
.map(|b| (0..8).map(|i| b + i * 8).collect::<Vec<u32>>())
.collect::<Vec<_>>();
assert_eq!(blocks(64, &[&keys]), expected);
let contiguous = (0..64).map(|i| key(i / 8)).collect::<Vec<_>>();
assert_eq!(
blocks(64, &[&keys, &contiguous]),
(0..64).map(|i| vec![i]).collect::<Vec<_>>(),
);
}
const N: u32 = 6;
fn partition(labels: &[Option<u32>]) -> SameVmctxPartition {
let keys = labels
.iter()
.map(|l| match l {
Some(l) => key(*l),
None => unknown(),
})
.collect::<Vec<_>>();
SameVmctxPartition::from_keys(&mut HashMap::new(), &keys)
}
fn decode(bytes: &[u8]) -> SameVmctxPartition {
let byte = |i: usize| bytes.get(i).copied().unwrap_or(0);
match byte(0) % 8 {
0 => SameVmctxPartition::Top,
1 => SameVmctxPartition::bottom(),
_ => {
let labels = (0..N)
.map(|i| match byte(1 + i as usize) % 5 {
4 => None,
l => Some(u32::from(l)),
})
.collect::<Vec<_>>();
partition(&labels)
}
}
}
fn decode3(bytes: &[u8]) -> (SameVmctxPartition, SameVmctxPartition, SameVmctxPartition) {
let chunk = 1 + N as usize;
let at = |i: usize| decode(bytes.get(i * chunk..).unwrap_or(&[]));
(at(0), at(1), at(2))
}
fn meet(a: &SameVmctxPartition, b: &SameVmctxPartition) -> SameVmctxPartition {
SameVmctxPartition::meet(&mut HashMap::new(), a.clone(), b.clone())
}
fn reference_meet(a: &SameVmctxPartition, b: &SameVmctxPartition) -> Vec<Vec<u32>> {
let mut assigned = vec![false; N as usize];
let mut blocks = Vec::new();
for i in 0..N {
if assigned[i as usize] {
continue;
}
let mut block = Vec::new();
for j in i..N {
let (x, y) = (FuncIndex::from_u32(i), FuncIndex::from_u32(j));
if !assigned[j as usize] && a.same_block(x, y) && b.same_block(x, y) {
assigned[j as usize] = true;
block.push(j);
}
}
blocks.push(block);
}
blocks
}
#[test]
fn top_and_bottom_are_the_lattice_bounds() {
let top = SameVmctxPartition::Top;
let bottom = SameVmctxPartition::bottom();
let p = partition(&[Some(0), Some(0), Some(1), Some(1), None, None]);
assert_eq!(meet(&top, &p).blocks(N), p.blocks(N));
assert_eq!(meet(&p, &top).blocks(N), p.blocks(N));
assert_eq!(meet(&bottom, &p).blocks(N), bottom.blocks(N));
assert_eq!(meet(&p, &bottom).blocks(N), bottom.blocks(N));
assert!(p.refines(&top, N));
assert!(bottom.refines(&p, N));
assert!(!top.refines(&p, N));
assert!(!p.refines(&bottom, N));
}
#[test]
fn meet_is_idempotent() {
for p in [
SameVmctxPartition::Top,
SameVmctxPartition::bottom(),
partition(&[Some(0), Some(0), Some(1), Some(1), Some(2), None]),
partition(&[Some(3), Some(3), Some(3), Some(3), Some(3), Some(3)]),
] {
assert_eq!(meet(&p, &p).blocks(N), p.blocks(N));
}
}
#[test]
fn meet_laws_hold_on_random_partitions() -> CheckResult<Vec<u8>> {
let mutator = m::default::<Vec<u8>>().map(|_ctx, bytes| {
bytes.truncate(3 * (1 + N as usize));
Ok(())
});
property_check().run_with(mutator, [Vec::new()], |bytes| {
let (a, b, c) = decode3(bytes);
let ab = meet(&a, &b);
let ba = meet(&b, &a);
assert_eq!(ab.blocks(N), ba.blocks(N), "meet is not commutative");
let ab_c = meet(&ab, &c);
let a_bc = meet(&a, &meet(&b, &c));
assert_eq!(ab_c.blocks(N), a_bc.blocks(N), "meet is not associative");
assert_eq!(
meet(&a, &a).blocks(N),
a.blocks(N),
"meet is not idempotent"
);
assert_eq!(
ab.blocks(N),
reference_meet(&a, &b),
"meet disagrees with the reference meet",
);
assert!(ab.refines(&a, N), "meet does not refine its left operand");
assert!(ab.refines(&b, N), "meet does not refine its right operand");
if c.refines(&a, N) && c.refines(&b, N) {
assert!(
c.refines(&ab, N),
"meet is not the greatest lower bound of its operands",
);
}
assert!(
meet(&ab, &c).refines(&meet(&a, &c), N),
"meet is not monotone in its left argument",
);
assert!(
meet(&c, &ab).refines(&meet(&c, &a), N),
"meet is not monotone in its right argument",
);
assert_eq!(
a.refines(&b, N),
ab.blocks(N) == a.blocks(N),
"`a <= b` and `a /\\ b == a` disagree",
);
Ok::<_, String>(())
})
}
#[test]
fn representatives_are_least_members_on_random_partitions() -> CheckResult<Vec<u8>> {
let mutator = m::default::<Vec<u8>>().map(|_ctx, bytes| {
bytes.truncate(1 + N as usize);
Ok(())
});
property_check().run_with(mutator, [Vec::new()], |bytes| {
let p = decode(bytes);
for block in p.blocks(N) {
let least = *block.first().unwrap();
for i in block {
assert_eq!(
p.representative(FuncIndex::from_u32(i)).as_u32(),
least,
"every member of a block must name the same representative",
);
}
}
Ok::<_, String>(())
})
}
}