use crate::adapter::generate_tier1_adapter;
use crate::contract::{validate_contract, ContractResult};
use colored::Colorize;
use cviz::model::{ComponentNode, CompositionGraph, ExportInfo, InterfaceConnection};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::PathBuf;
use wasmparser::collections::IndexSet;
pub const INST_PREFIX: &str = "my";
const PATH_PLACEHOLDER: &str = "/path/to/comp.wasm";
use crate::parse::config::{AdapterInjectionInfo, Injection, SpliceRule};
use crate::split::gen_split_path;
type InjectPlan = HashMap<usize, IndexSet<Injection>>;
struct Chain {
interface: Contract,
chain: Vec<u32>,
aliases: HashMap<u32, Option<String>>,
inject_plan: InjectPlan,
}
impl Chain {
fn consumer_split_path(
&self,
chain_idx: usize,
composition: &CompositionGraph,
splits_path: &str,
shim_comps: &HashMap<usize, usize>,
) -> Option<String> {
let consumer_id = *self.chain.get(chain_idx)?;
let split_to_use = resolved_split_num(consumer_id, composition, shim_comps);
Some(gen_split_path(splits_path, split_to_use))
}
}
#[derive(Clone, Debug)]
struct Contract {
name: String,
ty_fingerprint: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GeneratedAdapter {
pub adapter_path: String,
pub middleware_name: String,
pub target_interface: String,
pub tier1_interfaces: Vec<String>,
}
pub struct WacOutput {
pub wac: String,
pub wac_deps: BTreeMap<String, PathBuf>,
pub diagnostics: Vec<ContractResult>,
pub generated_adapters: Vec<GeneratedAdapter>,
}
pub fn generate_wac(
shim_comps: HashMap<usize, usize>,
splits_path: &str,
composition: &CompositionGraph,
rules: &[SpliceRule],
node_paths: Option<&HashMap<u32, PathBuf>>,
pkg_name: &str,
) -> anyhow::Result<WacOutput> {
warn_about_shim_resolutions(&shim_comps);
let mut wac_lines = vec![format!("package {pkg_name};")];
let mut handled_interfaces = HashSet::new();
let mut chains = vec![];
let mut ordered_node_ids = composition.nodes.keys().collect::<Vec<_>>();
ordered_node_ids.sort_by_key(|id| Reverse(**id));
for outer_node_id in ordered_node_ids {
let node = &composition.nodes[outer_node_id];
for InterfaceConnection {
interface_name,
source_instance,
is_host_import,
fingerprint,
..
} in node.imports.iter()
{
let mut chain = vec![*outer_node_id];
if *is_host_import {
continue;
}
let mut current_id = source_instance.unwrap();
chain.push(source_instance.unwrap());
while let Some(node) = composition.nodes.get(¤t_id) {
if let Some(conn) = node
.imports
.iter()
.find(|c| c.interface_name == *interface_name)
{
if !conn.is_host_import {
let src_id = conn.source_instance.unwrap();
chain.push(src_id);
current_id = src_id;
continue;
}
}
break;
}
if !handled_interfaces.contains(interface_name) && chain.len() > 1 {
chain.reverse();
chains.push(Chain {
interface: Contract {
name: interface_name.to_string(),
ty_fingerprint: fingerprint.clone(),
},
chain,
aliases: HashMap::new(),
inject_plan: HashMap::new(),
});
}
handled_interfaces.insert(interface_name.to_string());
}
}
for (
interface,
ExportInfo {
source_instance: source_inst,
fingerprint,
..
},
) in composition.component_exports.iter()
{
if handled_interfaces.contains(interface) {
continue;
}
chains.push(Chain {
interface: Contract {
name: interface.to_string(),
ty_fingerprint: fingerprint.clone(),
},
chain: vec![*source_inst],
aliases: HashMap::new(),
inject_plan: HashMap::new(),
});
}
let mut checked_middlewares = HashMap::new();
let mut diagnostics: Vec<ContractResult> = vec![];
let mut generated_adapters: Vec<GeneratedAdapter> = vec![];
for (rule_idx, rule) in rules.iter().enumerate() {
let mut any_interface_matched = false;
let mut any_full_match = false;
for chain in chains.iter_mut() {
let between = apply_rule_between(
rule,
chain,
composition,
splits_path,
&shim_comps,
&mut checked_middlewares,
&mut generated_adapters,
)?;
let before = apply_rule_before(
rule,
chain,
composition,
splits_path,
&shim_comps,
&mut checked_middlewares,
&mut generated_adapters,
)?;
any_interface_matched |= between.interface_matched | before.interface_matched;
any_full_match |= between.full_match | before.full_match;
diagnostics.extend(between.contract_results);
diagnostics.extend(before.contract_results);
}
if !any_full_match {
let iface = rule_interface(rule);
if !any_interface_matched {
let available: Vec<&str> =
chains.iter().map(|c| c.interface.name.as_str()).collect();
let iface_base = iface.split('@').next().unwrap_or(iface);
let possibly_intended: Vec<&str> = available
.iter()
.copied()
.filter(|&avail| {
let avail_base = avail.split('@').next().unwrap_or(avail);
avail_base == iface_base
|| avail.starts_with(iface)
|| iface.starts_with(avail)
})
.collect();
let intended_msg = if possibly_intended.is_empty() {
String::new()
} else {
format!(
"\n\t Possibly intended: [{}]",
possibly_intended.join(", ")
)
};
eprintln!(
"{}: rule {} — interface '{}' was not found in the composition.\n\
\t Available interfaces: [{}]{}",
"WARN".yellow().bold(),
rule_idx + 1,
iface,
available.join(", "),
intended_msg
);
} else {
let node_names: Vec<String> = chains
.iter()
.filter(|c| c.interface.name == iface)
.flat_map(|c| {
c.chain
.iter()
.map(|id| get_name(&composition.nodes[id]).to_string())
})
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
eprintln!(
"{}: rule {} — interface '{}' matched but no node names matched.\n\
\t Nodes on that interface: [{}]\n\
\t Check the 'name' fields in your config against these exactly.",
"WARN".yellow().bold(),
rule_idx + 1,
iface,
node_names.join(", ")
);
}
}
}
let mut mdl_override = None;
let mut last = String::new();
let mut instance_vars: HashMap<u32, String> = HashMap::new();
let mut split_to_var: HashMap<usize, String> = HashMap::new();
let mut outer_instances: HashMap<u32, String> = HashMap::new();
let mut used_comp_nodes: HashMap<u32, String> = HashMap::new();
let mut used_middlewares: Vec<(String, String)> = Vec::new();
let mut emitted_mdl_vars: std::collections::HashSet<String> = std::collections::HashSet::new();
let fan_in_consumers: HashSet<u32>;
{
let mut node_positions: HashMap<u32, BTreeSet<usize>> = HashMap::new();
for chain in &chains {
for (pos, &id) in chain.chain.iter().enumerate() {
node_positions.entry(id).or_default().insert(pos);
}
}
let mut non_zero_chain_count: HashMap<u32, usize> = HashMap::new();
for chain in &chains {
for (pos, &id) in chain.chain.iter().enumerate() {
if pos > 0 {
*non_zero_chain_count.entry(id).or_default() += 1;
}
}
}
fan_in_consumers = non_zero_chain_count
.into_iter()
.filter(|(_, n)| *n > 1)
.map(|(id, _)| id)
.collect();
let mut pure_providers: Vec<u32> = node_positions
.iter()
.filter(|(_, positions)| positions.iter().all(|&p| p == 0))
.map(|(&id, _)| id)
.collect();
pure_providers.sort();
let mut pre_pass_aliases: HashMap<u32, Option<String>> = HashMap::new();
for chain in &chains {
for (&id, alias) in &chain.aliases {
pre_pass_aliases.insert(id, alias.clone());
}
}
for node_id in pure_providers {
let node = &composition.nodes[&node_id];
get_or_create_inst(
node_id,
&pre_pass_aliases,
node,
&mut WacState {
instance_vars: &mut instance_vars,
used_comp_nodes: &mut used_comp_nodes,
wac_lines: &mut wac_lines,
},
&mut ShimDedup {
composition,
shim_comps: &shim_comps,
split_to_var: &mut split_to_var,
},
&None,
);
}
}
let mut fan_in_iface_vars: HashMap<u32, HashMap<String, String>> = HashMap::new();
let mut fan_in_aliases: HashMap<u32, HashMap<u32, Option<String>>> = HashMap::new();
let mut deferred_top_level_injects: Vec<DeferredTopLevelInject> = Vec::new();
let mut export_overrides: HashMap<(u32, String), String> = HashMap::new();
for Chain {
interface: chain_interface,
chain,
aliases,
inject_plan,
} in chains.iter()
{
for (i, id) in chain.iter().enumerate() {
let is_fan_in_last = fan_in_consumers.contains(id) && i == chain.len() - 1;
if chain.len() == 1 && is_fan_in_last {
if let Some(middlewares) = inject_plan.get(&(i + 1)) {
deferred_top_level_injects.push(DeferredTopLevelInject {
consumer_id: *id,
chain_interface: chain_interface.clone(),
middlewares: middlewares.clone(),
});
}
continue;
}
if !is_fan_in_last {
let node = &composition.nodes[id];
let node_var = get_or_create_inst(
*id,
aliases,
node,
&mut WacState {
instance_vars: &mut instance_vars,
used_comp_nodes: &mut used_comp_nodes,
wac_lines: &mut wac_lines,
},
&mut ShimDedup {
composition,
shim_comps: &shim_comps,
split_to_var: &mut split_to_var,
},
&mdl_override,
);
last = node_var;
mdl_override = Some((chain_interface.clone(), last.clone()));
}
if let Some(middlewares) = inject_plan.get(&(i + 1)) {
let reversed_list = reverse_set(middlewares);
for mdl in reversed_list.iter() {
if let Some(adapter_info) = &mdl.adapter_info {
let (adapter_var, extra_args) = create_tier1_mdl(
&last,
mdl,
chain_interface,
adapter_info,
composition,
&shim_comps,
&mut wac_lines,
&mut emitted_mdl_vars,
)?;
last = adapter_var;
used_middlewares.extend(extra_args);
} else {
last = create_mdl(&last, &mdl.name, chain_interface, &mut wac_lines);
used_middlewares.push((
last.clone(),
mdl.path
.as_ref()
.cloned()
.unwrap_or(PATH_PLACEHOLDER.to_string()),
));
}
mdl_override = Some((chain_interface.clone(), last.clone()));
}
}
if is_fan_in_last {
fan_in_iface_vars
.entry(*id)
.or_default()
.insert(chain_interface.name.clone(), last.clone());
fan_in_aliases.entry(*id).or_insert_with(|| aliases.clone());
} else if i == chain.len() - 1 {
outer_instances.insert(*id, last.clone());
}
}
}
for (consumer_id, iface_vars) in fan_in_iface_vars.iter() {
let consumer_node = &composition.nodes[consumer_id];
let aliases = fan_in_aliases.get(consumer_id).unwrap();
let alias = aliases.get(consumer_id).cloned();
let pkg = if let Some(Some(a)) = alias {
a
} else {
sanitize_wac_id(get_name(consumer_node))
};
used_comp_nodes.insert(*consumer_id, pkg.clone());
let node_var = instance_vars
.entry(*consumer_id)
.or_insert_with(|| pkg.clone())
.clone();
let mut line = format!("let {node_var} = new {INST_PREFIX}:{pkg} {{");
for conn in &consumer_node.imports {
if !conn.is_host_import {
let iface = &conn.interface_name;
let src_var = if let Some(v) = iface_vars.get(iface) {
v.clone()
} else if let Some(v) = conn.source_instance.and_then(|id| instance_vars.get(&id)) {
v.clone()
} else {
continue;
};
line.push_str(&format!("\n \"{iface}\": {src_var}[\"{iface}\"],"));
}
}
line.push_str("\n ...\n};");
wac_lines.push(line);
outer_instances.insert(*consumer_id, node_var.clone());
}
for deferred in deferred_top_level_injects {
let Some(consumer_var) = instance_vars.get(&deferred.consumer_id).cloned() else {
anyhow::bail!(
"deferred top-level inject for instance {} but no var was created \
(fan-in pass should have instantiated it); please file a bug",
deferred.consumer_id
);
};
let mut current_provider = consumer_var;
for mdl in reverse_set(&deferred.middlewares).iter() {
if let Some(adapter_info) = &mdl.adapter_info {
let (adapter_var, extra_args) = create_tier1_mdl(
¤t_provider,
mdl,
&deferred.chain_interface,
adapter_info,
composition,
&shim_comps,
&mut wac_lines,
&mut emitted_mdl_vars,
)?;
current_provider = adapter_var;
used_middlewares.extend(extra_args);
} else {
current_provider = create_mdl(
¤t_provider,
&mdl.name,
&deferred.chain_interface,
&mut wac_lines,
);
used_middlewares.push((
current_provider.clone(),
mdl.path
.as_ref()
.cloned()
.unwrap_or(PATH_PLACEHOLDER.to_string()),
));
}
}
export_overrides.insert(
(deferred.consumer_id, deferred.chain_interface.name),
current_provider,
);
}
for (
export_name,
ExportInfo {
source_instance: outer_inst_id,
..
},
) in composition.component_exports.iter()
{
if handled_interfaces.contains(export_name) && !outer_instances.contains_key(outer_inst_id)
{
continue;
}
let effective_inst_id = resolve_shim_node(*outer_inst_id, composition, &shim_comps);
let node_var = if let Some(override_var) =
export_overrides.get(&(effective_inst_id, export_name.clone()))
{
override_var.clone()
} else if let Some(generated_outer) = outer_instances.get(&effective_inst_id) {
generated_outer.clone()
} else {
let outer_node = &composition.nodes[&effective_inst_id];
get_or_create_inst(
effective_inst_id,
&HashMap::new(),
outer_node,
&mut WacState {
instance_vars: &mut instance_vars,
used_comp_nodes: &mut used_comp_nodes,
wac_lines: &mut wac_lines,
},
&mut ShimDedup {
composition,
shim_comps: &shim_comps,
split_to_var: &mut split_to_var,
},
&None,
)
};
let export_line = format!("export {node_var}[\"{export_name}\"];");
wac_lines.push(export_line);
}
let args = gen_wac_args(
shim_comps,
splits_path,
composition,
&used_comp_nodes,
&used_middlewares,
node_paths,
);
Ok(WacOutput {
wac: wac_lines.join("\n\n"),
wac_deps: args,
diagnostics,
generated_adapters,
})
}
fn gen_wac_args(
shim_comps: HashMap<usize, usize>,
splits_path: &str,
graph: &CompositionGraph,
used_comps: &HashMap<u32, String>,
used_mdls: &Vec<(String, String)>,
node_paths: Option<&HashMap<u32, PathBuf>>,
) -> BTreeMap<String, PathBuf> {
let mut deps: BTreeMap<String, PathBuf> = BTreeMap::new();
for (inst_id, name) in used_comps.iter() {
let comp_path: PathBuf = if let Some(paths) = node_paths {
paths
.get(inst_id)
.cloned()
.unwrap_or_else(|| PathBuf::from(PATH_PLACEHOLDER))
} else {
let split_to_use = resolved_split_num(*inst_id, graph, &shim_comps);
PathBuf::from(gen_split_path(splits_path, split_to_use))
};
deps.insert(format!("{INST_PREFIX}:{name}"), comp_path);
}
for (mw_name, mw_path) in used_mdls {
deps.insert(format!("{INST_PREFIX}:{mw_name}"), PathBuf::from(mw_path));
}
deps
}
fn resolve_shim(mut component_num: usize, shim_comps: &HashMap<usize, usize>) -> usize {
while is_shim_split_num(component_num, shim_comps) {
component_num = shim_comps[&component_num];
}
component_num
}
fn node_split_num(node_id: u32, composition: &CompositionGraph) -> usize {
(composition.nodes[&node_id].component_num + 1) as usize
}
fn resolved_split_num(
node_id: u32,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> usize {
resolve_shim(node_split_num(node_id, composition), shim_comps)
}
fn warn_about_shim_resolutions(shim_comps: &HashMap<usize, usize>) {
let mut shim_keys: Vec<usize> = shim_comps.keys().copied().collect();
shim_keys.sort();
for shim_num in shim_keys {
let resolved = resolve_shim(shim_num, shim_comps);
if resolved != shim_num {
eprintln!(
"{}: {}",
"WARN".yellow().bold(),
format!(
"\tAssumption made! It is likely that split{shim_num} is a shim component,\n\
\tdefaulting to split{resolved} instead in the generated wac command!\n\
\tIf this assumption is incorrect, modify the generated wac command."
)
.yellow()
);
}
}
}
struct DeferredTopLevelInject {
consumer_id: u32,
chain_interface: Contract,
middlewares: IndexSet<Injection>,
}
struct RuleApplyResult {
contract_results: Vec<ContractResult>,
interface_matched: bool,
full_match: bool,
}
#[allow(clippy::too_many_arguments)]
fn apply_rule_between(
rule: &SpliceRule,
chain: &mut Chain,
composition: &CompositionGraph,
splits_path: &str,
shim_comps: &HashMap<usize, usize>,
checked_middlewares: &mut HashMap<String, BTreeMap<String, ExportInfo>>,
generated_adapters: &mut Vec<GeneratedAdapter>,
) -> anyhow::Result<RuleApplyResult> {
let mut contract_results = vec![];
let mut interface_matched = false;
let mut full_match = false;
if let SpliceRule::Between {
interface,
inner_name,
inner_alias,
outer_name,
outer_alias,
inject,
} = rule
{
for (i, window) in chain.chain.windows(2).enumerate() {
let inner_id = window[0];
let outer_id = window[1];
let inner_node = &composition.nodes[&inner_id];
let outer_node = &composition.nodes[&outer_id];
let inner_var = get_name(inner_node).to_string();
let outer_var = get_name(outer_node).to_string();
if *interface != chain.interface.name {
continue;
}
interface_matched = true;
if *inner_name == inner_var && *outer_name == outer_var {
full_match = true;
let new_aliases = vec![
(inner_id, inner_alias.clone()),
(outer_id, outer_alias.clone()),
];
let consumer_path =
chain.consumer_split_path(i + 1, composition, splits_path, shim_comps);
contract_results.extend(add_to_inject_plan(
interface,
inject,
i + 1,
&new_aliases,
&mut chain.aliases,
&mut chain.inject_plan,
&chain.interface.ty_fingerprint,
splits_path,
consumer_path,
checked_middlewares,
generated_adapters,
)?);
}
}
}
Ok(RuleApplyResult {
contract_results,
interface_matched,
full_match,
})
}
#[allow(clippy::too_many_arguments)]
fn apply_rule_before(
rule: &SpliceRule,
chain: &mut Chain,
composition: &CompositionGraph,
splits_path: &str,
shim_comps: &HashMap<usize, usize>,
checked_middlewares: &mut HashMap<String, BTreeMap<String, ExportInfo>>,
generated_adapters: &mut Vec<GeneratedAdapter>,
) -> anyhow::Result<RuleApplyResult> {
let mut contract_results = vec![];
let mut interface_matched = false;
let mut full_match = false;
if let SpliceRule::Before {
interface,
provider_name,
provider_alias,
inject,
} = rule
{
for (i, id) in chain.chain.iter().enumerate() {
if *interface != chain.interface.name {
continue;
}
interface_matched = true;
let outer_node = &composition.nodes[id];
if let Some(provider) = provider_name {
if get_name(outer_node) != *provider {
continue;
}
}
full_match = true;
let new_aliases = vec![(*id, provider_alias.clone())];
let consumer_path = chain
.consumer_split_path(i + 1, composition, splits_path, shim_comps)
.or_else(|| chain.consumer_split_path(i, composition, splits_path, shim_comps));
contract_results.extend(add_to_inject_plan(
interface,
inject,
i + 1,
&new_aliases,
&mut chain.aliases,
&mut chain.inject_plan,
&chain.interface.ty_fingerprint,
splits_path,
consumer_path,
checked_middlewares,
generated_adapters,
)?);
}
}
Ok(RuleApplyResult {
contract_results,
interface_matched,
full_match,
})
}
#[allow(clippy::too_many_arguments)]
fn add_to_inject_plan(
interface_name: &str,
to_inject: &[Injection],
chain_idx: usize,
new_aliases: &[(u32, Option<String>)],
aliases: &mut HashMap<u32, Option<String>>,
inject_plan: &mut InjectPlan,
contract_fingerprint: &Option<String>,
splits_path: &str,
consumer_split: Option<String>,
checked_middlewares: &mut HashMap<String, BTreeMap<String, ExportInfo>>,
generated_adapters: &mut Vec<GeneratedAdapter>,
) -> anyhow::Result<Vec<ContractResult>> {
let contract_results = validate_contract(
to_inject,
interface_name,
contract_fingerprint,
checked_middlewares,
);
let mut resolved: Vec<Injection> = Vec::with_capacity(to_inject.len());
let mut final_results: Vec<ContractResult> = Vec::with_capacity(contract_results.len());
for (injection, result) in to_inject.iter().zip(contract_results) {
match result {
ContractResult::Tier1Compatible(matched_interfaces) => {
let consumer_split_path = consumer_split.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"No consumer/provider split available for interface '{interface_name}' \
while generating adapter for middleware '{}'. Please open an issue \
with a repro at https://github.com/ejrgilbert/splicer/issues",
injection.name
)
})?;
let adapter_path = generate_tier1_adapter(
&injection.name,
interface_name,
&matched_interfaces,
splits_path,
consumer_split_path,
)?;
generated_adapters.push(GeneratedAdapter {
adapter_path: adapter_path.clone(),
middleware_name: injection.name.clone(),
target_interface: interface_name.to_string(),
tier1_interfaces: matched_interfaces.clone(),
});
resolved.push(Injection {
name: injection.name.clone(),
path: injection.path.clone(),
adapter_info: Some(AdapterInjectionInfo {
adapter_path,
tier1_interfaces: matched_interfaces,
}),
});
}
other => {
resolved.push(injection.clone());
final_results.push(other);
}
}
}
let middlewares = inject_plan
.entry(chain_idx)
.or_insert(IndexSet::from_iter(resolved.iter().cloned()));
for (inst_id, new_alias) in new_aliases {
if let (Some(new_alias), Some(Some(configured_alias))) = (new_alias, aliases.get(inst_id)) {
if new_alias != configured_alias {
anyhow::bail!(
"Internal error: alias conflict for interface '{interface_name}' — \
was configured as '{configured_alias}', but the tool prepared it as \
'{new_alias}' in some previous injection pass. Please report this bug."
);
}
}
aliases.insert(*inst_id, new_alias.clone());
}
middlewares.extend(resolved);
Ok(final_results)
}
struct ShimDedup<'a> {
composition: &'a CompositionGraph,
shim_comps: &'a HashMap<usize, usize>,
split_to_var: &'a mut HashMap<usize, String>,
}
struct WacState<'a> {
instance_vars: &'a mut HashMap<u32, String>,
used_comp_nodes: &'a mut HashMap<u32, String>,
wac_lines: &'a mut Vec<String>,
}
fn get_or_create_inst(
inst_id: u32,
aliases: &HashMap<u32, Option<String>>,
node: &ComponentNode,
state: &mut WacState,
dedup: &mut ShimDedup,
with_override: &Option<(Contract, String)>,
) -> String {
if let Some(var) = state.instance_vars.get(&inst_id) {
return var.clone();
}
let resolved_split = resolved_split_num(inst_id, dedup.composition, dedup.shim_comps);
if let Some(existing_var) = dedup.split_to_var.get(&resolved_split) {
state.instance_vars.insert(inst_id, existing_var.clone());
return existing_var.clone();
}
let alias = aliases.get(&inst_id).cloned();
let pkg = if let Some(Some(alias)) = alias {
alias.clone()
} else {
sanitize_wac_id(get_name(node))
};
state.used_comp_nodes.insert(inst_id, pkg.clone());
let node_var = state
.instance_vars
.entry(inst_id)
.or_insert_with(|| pkg.clone())
.clone();
dedup.split_to_var.insert(resolved_split, node_var.clone());
let mut line = format!("let {node_var} = new {INST_PREFIX}:{pkg} {{");
for conn in &node.imports {
if !conn.is_host_import {
let src_id = conn.source_instance;
if let Some((
Contract {
name: override_interface,
..
},
override_var,
)) = &with_override
{
let src_var = if conn.interface_name == *override_interface {
override_var.clone()
} else if let Some(src_var) = state.instance_vars.get(&src_id.unwrap()) {
src_var.clone()
} else {
continue;
};
line.push_str(&format!(
"\n \"{iface}\": {src}[\"{iface}\"],",
iface = conn.interface_name,
src = src_var
));
}
}
}
line.push_str("\n ...\n};");
state.wac_lines.push(line);
node_var
}
fn create_mdl(
input_inst: &String,
mw: &String,
interface: &Contract,
wac_lines: &mut Vec<String>,
) -> String {
let mw_line = format!(
"let {mw} = new {INST_PREFIX}:{mw} {{\n \"{interface}\": {input_inst}[\"{interface}\"], ...\n}};",
interface = interface.name,
);
wac_lines.push(mw_line);
mw.clone()
}
#[allow(clippy::too_many_arguments)]
fn create_tier1_mdl(
downstream_inst: &str,
mdl: &Injection,
interface: &Contract,
adapter_info: &AdapterInjectionInfo,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
wac_lines: &mut Vec<String>,
emitted_mdl_vars: &mut std::collections::HashSet<String>,
) -> anyhow::Result<(String, Vec<(String, String)>)> {
let real_var = mdl.name.clone();
let adapter_var = format!("{}-adapter-{}", mdl.name, sanitize_wac_id(&interface.name));
if emitted_mdl_vars.insert(real_var.clone()) {
wac_lines.push(format!(
"let {real_var} = new {INST_PREFIX}:{real_var} {{ ... }};"
));
}
use crate::contract::{versioned_interface, TIER1_VERSION};
let mut adapter_line = format!(
"let {adapter_var} = new {INST_PREFIX}:{adapter_var} {{\n \"{iface}\": {downstream_inst}[\"{iface}\"],",
iface = interface.name,
);
for tier1_iface in &adapter_info.tier1_interfaces {
let versioned = versioned_interface(tier1_iface, TIER1_VERSION);
adapter_line.push_str(&format!(
"\n \"{versioned}\": {real_var}[\"{versioned}\"],"
));
}
if let Ok(adapter_bytes) = std::fs::read(&adapter_info.adapter_path) {
for extra in factored_types_to_wire(
&resource_bearing_imports(&adapter_bytes),
&interface.name,
composition,
shim_comps,
)? {
adapter_line.push_str(&format!(
"\n \"{extra}\": {downstream_inst}[\"{extra}\"],"
));
}
}
adapter_line.push_str("\n ...\n};");
wac_lines.push(adapter_line);
let used = vec![
(
real_var,
mdl.path
.as_ref()
.cloned()
.unwrap_or(PATH_PLACEHOLDER.to_string()),
),
(adapter_var.clone(), adapter_info.adapter_path.clone()),
];
Ok((adapter_var, used))
}
fn rule_interface(rule: &SpliceRule) -> &str {
match rule {
SpliceRule::Before { interface, .. } => interface,
SpliceRule::Between { interface, .. } => interface,
}
}
fn get_name(node: &ComponentNode) -> &str {
node.display_label()
}
fn factored_types_to_wire(
resource_imports: &[String],
target_iface: &str,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> anyhow::Result<Vec<String>> {
let providers = |iface: &str| -> std::collections::HashSet<usize> {
let mut out = std::collections::HashSet::new();
if let Some(info) = composition.component_exports.get(iface) {
out.insert(resolved_split_num(
info.source_instance,
composition,
shim_comps,
));
}
for node in composition.nodes.values() {
for conn in &node.imports {
if conn.interface_name != iface || conn.is_host_import {
continue;
}
if let Some(src) = conn.source_instance {
out.insert(resolved_split_num(src, composition, shim_comps));
}
}
}
out
};
let target_providers = providers(target_iface);
let mut out = Vec::new();
for extra in resource_imports {
if extra == target_iface {
continue;
}
let extra_providers = providers(extra);
if extra_providers.is_empty() {
continue; }
if extra_providers != target_providers {
anyhow::bail!(
"splicer can't yet wire factored-types interface `{extra}` for adapter \
on `{target_iface}`: the resource-bearing types interface is exported \
by a different component than the target. Splicer's tier-1 wiring \
currently assumes both interfaces come from the same provider. \
Workaround: have one component export both interfaces."
);
}
out.push(extra.clone());
}
Ok(out)
}
fn resource_bearing_imports(bytes: &[u8]) -> Vec<String> {
let Ok(decoded) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
wit_component::decode(bytes)
})) else {
return Vec::new();
};
let Ok(wit_component::DecodedWasm::Component(resolve, world_id)) = decoded else {
return Vec::new();
};
let world = &resolve.worlds[world_id];
let mut result = Vec::new();
for (_key, item) in &world.imports {
let wit_parser::WorldItem::Interface { id, .. } = item else {
continue;
};
let iface = &resolve.interfaces[*id];
let has_resource = iface
.types
.values()
.any(|tid| matches!(resolve.types[*tid].kind, wit_parser::TypeDefKind::Resource));
if !has_resource {
continue;
}
if let Some(name) = resolve.id_of(*id) {
result.push(name);
}
}
result
}
fn is_shim_split_num(split_num: usize, shim_comps: &HashMap<usize, usize>) -> bool {
shim_comps.contains_key(&split_num)
}
fn resolve_shim_node(
inst_id: u32,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> u32 {
if !composition.nodes.contains_key(&inst_id) {
return inst_id;
}
let split_num = node_split_num(inst_id, composition);
let resolved = resolved_split_num(inst_id, composition, shim_comps);
if resolved == split_num {
return inst_id;
}
composition
.nodes
.iter()
.find(|(_, n)| (n.component_num + 1) as usize == resolved)
.map(|(id, _)| *id)
.unwrap_or(inst_id)
}
fn sanitize_wac_id(raw: &str) -> String {
let sanitized = raw.replace([':', '/', '.', '_', '@'], "-");
let stripped = sanitized
.strip_prefix(&format!("{INST_PREFIX}-"))
.unwrap_or(&sanitized);
stripped
.split('-')
.map(|seg| match seg.chars().next() {
Some(c) if c.is_ascii_digit() => format!("v{seg}"),
_ => seg.to_string(),
})
.collect::<Vec<_>>()
.join("-")
}
fn reverse_set(set: &IndexSet<Injection>) -> Vec<Injection> {
let mut res = vec![];
for item in set.iter() {
res.insert(0, item.clone());
}
res
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_graph(n_nodes: u32, edges: &[(u32, &str, Option<u32>, bool)]) -> CompositionGraph {
let mut graph = CompositionGraph::new();
let mut nodes: HashMap<u32, ComponentNode> = HashMap::new();
for i in 0..n_nodes {
nodes.insert(i, ComponentNode::new(format!("$node-{i}"), i, i));
}
for (consumer, iface, src, is_host) in edges {
let n = nodes.get_mut(consumer).expect("node id in range");
n.add_import(InterfaceConnection {
interface_name: iface.to_string(),
source_instance: *src,
is_host_import: *is_host,
fingerprint: None,
interface_type: None,
});
}
for (i, node) in nodes {
graph.add_node(i, node);
}
graph
}
#[test]
fn factored_types_same_provider_wires() {
let graph = synth_graph(
2,
&[
(1, "my:shape/api@1.0.0", Some(0), false),
(1, "my:shape/types@1.0.0", Some(0), false),
],
);
let extras = factored_types_to_wire(
&["my:shape/types@1.0.0".to_string()],
"my:shape/api@1.0.0",
&graph,
&HashMap::new(),
)
.expect("same-provider factored types should wire");
assert_eq!(extras, vec!["my:shape/types@1.0.0".to_string()]);
}
#[test]
fn factored_types_host_provided_skipped() {
let graph = synth_graph(
2,
&[
(0, "wasi:http/handler@0.3.0", Some(1), false),
(0, "wasi:http/types@0.3.0", None, true),
],
);
let extras = factored_types_to_wire(
&["wasi:http/types@0.3.0".to_string()],
"wasi:http/handler@0.3.0",
&graph,
&HashMap::new(),
)
.expect("host-provided types should be skipped, not error");
assert!(extras.is_empty());
}
#[test]
fn factored_types_multi_provider_bails() {
let graph = synth_graph(
3,
&[
(2, "my:shape/api@1.0.0", Some(1), false),
(2, "my:shape/types@1.0.0", Some(0), false),
],
);
let err = factored_types_to_wire(
&["my:shape/types@1.0.0".to_string()],
"my:shape/api@1.0.0",
&graph,
&HashMap::new(),
)
.expect_err("multi-provider factored types should bail");
let msg = err.to_string();
assert!(
msg.contains("my:shape/types@1.0.0"),
"error should name the offending interface; got: {msg}"
);
assert!(
msg.contains("different component"),
"error should explain the multi-provider problem; got: {msg}"
);
}
}