use crate::adapter::{generate_tier1_adapter, generate_tier2_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";
pub const EDGE_ID_CONFIG_KEY: &str = "_splicer_edge_id";
pub fn derive_edge_id(interface: &str, from: Option<&str>, to: &str) -> String {
match from {
Some(caller) => format!("{interface}::{caller}->{to}"),
None => format!("{interface}::->{to}"),
}
}
use crate::parse::config::{AdapterInjectionInfo, Injection, SpliceRule};
use crate::split::gen_split_path;
use anyhow::Context;
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 matched_hook_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 any_rule_matched: bool,
}
#[derive(Debug, Clone)]
struct Entity {
var: String,
pkg: String,
imports: Vec<(String, String)>,
catchall: bool,
}
impl Entity {
fn leaf(var: impl Into<String>, pkg: impl Into<String>) -> Self {
Self {
var: var.into(),
pkg: pkg.into(),
imports: vec![],
catchall: true,
}
}
fn wired(
var: impl Into<String>,
pkg: impl Into<String>,
imports: Vec<(String, String)>,
) -> Self {
Self {
var: var.into(),
pkg: pkg.into(),
imports,
catchall: true,
}
}
fn deps(&self) -> impl Iterator<Item = &str> {
self.imports.iter().map(|(_, v)| v.as_str())
}
fn render(&self) -> String {
if self.imports.is_empty() && !self.catchall {
return format!("let {} = new {INST_PREFIX}:{} {{}};", self.var, self.pkg);
}
let mut s = format!("let {} = new {INST_PREFIX}:{} {{", self.var, self.pkg);
for (iface, src) in &self.imports {
s.push_str(&format!("\n \"{iface}\": {src}[\"{iface}\"],"));
}
if self.catchall {
s.push_str("\n ...");
}
s.push_str("\n};");
s
}
}
#[derive(Debug, Default)]
struct EmitPlan {
entities: BTreeMap<String, Entity>,
exports: Vec<(String, String)>,
used_comp_nodes: HashMap<u32, String>,
used_middlewares: BTreeMap<String, String>,
aliases: HashMap<u32, Option<String>>,
node_vars: HashMap<u32, String>,
routing: HashMap<(u32, String), String>,
export_routing: HashMap<(u32, String), String>,
simple_mdl_counts: HashMap<String, usize>,
adapter_counts: HashMap<String, usize>,
emitted_real_vars: HashSet<String>,
}
impl EmitPlan {
fn new() -> Self {
Self::default()
}
fn with_aliases(mut self, chains: &[Chain]) -> Self {
for chain in chains {
for (id, alias) in &chain.aliases {
self.aliases.entry(*id).or_insert_with(|| alias.clone());
}
}
self
}
fn with_node_vars(
mut self,
composition: &CompositionGraph,
chains: &[Chain],
shim_comps: &HashMap<usize, usize>,
) -> Self {
let mut chain_nodes: BTreeSet<u32> = BTreeSet::new();
for chain in chains {
for id in &chain.chain {
chain_nodes.insert(*id);
}
}
let mut split_to_var: HashMap<usize, String> = HashMap::new();
for id in &chain_nodes {
let resolved_split = resolved_split_num(*id, composition, shim_comps);
if let Some(existing) = split_to_var.get(&resolved_split) {
self.node_vars.insert(*id, existing.clone());
continue;
}
let pkg = match self.aliases.get(id) {
Some(Some(a)) => a.clone(),
_ => sanitize_wac_id(get_name(&composition.nodes[id])),
};
self.node_vars.insert(*id, pkg.clone());
split_to_var.insert(resolved_split, pkg);
}
self
}
fn with_chain_routing(
mut self,
chains: &[Chain],
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> anyhow::Result<Self> {
for Chain {
interface,
chain,
inject_plan,
..
} in chains
{
let mut prev_var: Option<String> = None;
for i in 0..chain.len() {
let id = chain[i];
let node_var = self.node_vars.get(&id).cloned().ok_or_else(|| {
anyhow::anyhow!(
"with_chain_routing: chain participant {id} has no var; \
with_node_vars must run before with_chain_routing"
)
})?;
let routing_id = resolve_shim_node(id, composition, shim_comps);
if i > 0 {
let upstream = prev_var.clone().ok_or_else(|| {
anyhow::anyhow!(
"with_chain_routing: chain position {i} has no upstream var; \
chain construction produced an inconsistent ordering"
)
})?;
let current = self.fold_inject_middlewares(
upstream,
inject_plan.get(&i),
interface,
composition,
shim_comps,
)?;
self.routing
.insert((routing_id, interface.name.clone()), current);
}
prev_var = Some(node_var.clone());
if i == chain.len() - 1 {
if let Some(top) = inject_plan.get(&(i + 1)) {
if !top.is_empty() {
let current = self.fold_inject_middlewares(
node_var.clone(),
Some(top),
interface,
composition,
shim_comps,
)?;
self.export_routing
.insert((routing_id, interface.name.clone()), current);
}
}
}
}
}
Ok(self)
}
fn with_node_entities(
mut self,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> anyhow::Result<Self> {
let mut emitted: HashSet<String> = HashSet::new();
let mut node_ids: Vec<u32> = self.node_vars.keys().copied().collect();
node_ids.sort();
for id in node_ids {
let var = self.node_vars[&id].clone();
if !emitted.insert(var.clone()) {
continue;
}
if self.entities.contains_key(&var) {
anyhow::bail!(
"WAC var name collision: composition node `{var}` \
conflicts with a previously-emitted middleware entity \
(rename the node alias or the middleware's `name:`)"
);
}
let node = &composition.nodes[&id];
let routing_id = resolve_shim_node(id, composition, shim_comps);
let mut imports: Vec<(String, String)> = Vec::new();
for conn in &node.imports {
if conn.is_host_import {
continue;
}
let iface = &conn.interface_name;
let src_var = if let Some(routed) = self.routing.get(&(routing_id, iface.clone())) {
routed.clone()
} else if let Some(src_id) = conn.source_instance {
match self.node_vars.get(&src_id) {
Some(v) => v.clone(),
None => continue,
}
} else {
continue;
};
imports.push((iface.clone(), src_var));
}
self.entities.insert(
var.clone(),
Entity::wired(var.clone(), var.clone(), imports),
);
self.used_comp_nodes.insert(id, var);
}
Ok(self)
}
fn with_exports(
mut self,
composition: &CompositionGraph,
chains: &[Chain],
handled_interfaces: &HashSet<String>,
shim_comps: &HashMap<usize, usize>,
) -> Self {
let mut chain_consumers: HashSet<u32> = HashSet::new();
for chain in chains {
if let Some(last) = chain.chain.last() {
chain_consumers.insert(resolve_shim_node(*last, composition, shim_comps));
}
}
for (export_iface, info) in composition.component_exports.iter() {
let effective_id = resolve_shim_node(info.source_instance, composition, shim_comps);
if handled_interfaces.contains(export_iface) && !chain_consumers.contains(&effective_id)
{
continue;
}
let export_var = if let Some(wrapped) = self
.export_routing
.get(&(effective_id, export_iface.clone()))
{
wrapped.clone()
} else if let Some(v) = self.node_vars.get(&effective_id) {
v.clone()
} else {
continue;
};
self.exports.push((export_iface.clone(), export_var));
}
self
}
fn fold_inject_middlewares(
&mut self,
initial: String,
middlewares: Option<&IndexSet<Injection>>,
interface: &Contract,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> anyhow::Result<String> {
let Some(middlewares) = middlewares else {
return Ok(initial);
};
let ordered: Vec<&Injection> = middlewares.iter().collect();
let mut current = initial;
for mdl in ordered.iter().rev() {
current = self.add_middleware(mdl, interface, ¤t, composition, shim_comps)?;
}
Ok(current)
}
fn add_middleware(
&mut self,
mdl: &Injection,
interface: &Contract,
downstream_var: &str,
composition: &CompositionGraph,
shim_comps: &HashMap<usize, usize>,
) -> anyhow::Result<String> {
use crate::contract::{
versioned_interface, TIER1_PACKAGE, TIER1_VERSION, TIER2_PACKAGE, TIER2_VERSION,
};
let real_pkg = mdl.name.clone();
let mdl_path = mdl
.path
.as_ref()
.cloned()
.unwrap_or_else(|| PATH_PLACEHOLDER.to_string());
if let Some(adapter_info) = &mdl.adapter_info {
let adapter_pkg = format!("{}-adapter-{}", mdl.name, sanitize_wac_id(&interface.name));
let cfg_pkg = mdl
.config_provider_path
.as_ref()
.map(|_| format!("{real_pkg}-config"));
if self.emitted_real_vars.insert(real_pkg.clone()) {
self.used_middlewares.insert(real_pkg.clone(), mdl_path);
match cfg_pkg.as_ref() {
Some(cfg_pkg) => {
self.entities
.insert(cfg_pkg.clone(), Entity::leaf(cfg_pkg, cfg_pkg));
self.used_middlewares.insert(
cfg_pkg.clone(),
mdl.config_provider_path.as_ref().unwrap().clone(),
);
self.entities.insert(
real_pkg.clone(),
Entity::wired(
&real_pkg,
&real_pkg,
vec![(
crate::config_provider::BUILTIN_CONFIG_GET_VERSIONED
.to_string(),
cfg_pkg.clone(),
)],
),
);
}
None => {
self.entities
.insert(real_pkg.clone(), Entity::leaf(&real_pkg, &real_pkg));
}
}
}
let adapter_var = disambiguated_var(&mut self.adapter_counts, &adapter_pkg);
let mut imports: Vec<(String, String)> =
vec![(interface.name.clone(), downstream_var.to_string())];
for hook_iface in &adapter_info.matched_hook_interfaces {
let version = if hook_iface.starts_with(&format!("{TIER1_PACKAGE}/")) {
TIER1_VERSION
} else if hook_iface.starts_with(&format!("{TIER2_PACKAGE}/")) {
TIER2_VERSION
} else {
anyhow::bail!(
"matched hook interface '{hook_iface}' is not part of any known tier package",
);
};
imports.push((versioned_interface(hook_iface, version), real_pkg.clone()));
}
let adapter_bytes = std::fs::read(&adapter_info.adapter_path).map_err(|e| {
anyhow::anyhow!(
"failed to read freshly-generated adapter wasm at `{}`: {e}",
adapter_info.adapter_path,
)
})?;
for extra in factored_types_to_wire(
&resource_bearing_imports(&adapter_bytes),
&interface.name,
composition,
shim_comps,
)? {
imports.push((extra, downstream_var.to_string()));
}
self.entities.insert(
adapter_var.clone(),
Entity::wired(&adapter_var, &adapter_pkg, imports),
);
self.used_middlewares
.insert(adapter_pkg, adapter_info.adapter_path.clone());
Ok(adapter_var)
} else {
let mw_var = disambiguated_var(&mut self.simple_mdl_counts, &real_pkg);
let imports = if mdl.tier.is_some_and(|t| !t.imports_target()) {
Vec::new()
} else {
vec![(interface.name.clone(), downstream_var.to_string())]
};
self.entities
.insert(mw_var.clone(), Entity::wired(&mw_var, &real_pkg, imports));
self.used_middlewares.insert(real_pkg, mdl_path);
Ok(mw_var)
}
}
fn render(&self, pkg_name: &str) -> anyhow::Result<String> {
let mut lines = vec![format!("package {pkg_name};")];
for var in self.topo_sort()? {
lines.push(self.entities[&var].render());
}
for (iface, src) in &self.exports {
lines.push(format!("export {src}[\"{iface}\"];"));
}
Ok(lines.join("\n\n"))
}
fn topo_sort(&self) -> anyhow::Result<Vec<String>> {
let mut in_degree: BTreeMap<String, usize> = BTreeMap::new();
let mut dependents: BTreeMap<String, Vec<String>> = BTreeMap::new();
for var in self.entities.keys() {
in_degree.insert(var.clone(), 0);
}
for (var, entity) in &self.entities {
let mut seen: HashSet<&str> = HashSet::new();
for dep in entity.deps() {
if dep == var.as_str() || !self.entities.contains_key(dep) || !seen.insert(dep) {
continue;
}
*in_degree.get_mut(var).unwrap() += 1;
dependents
.entry(dep.to_string())
.or_default()
.push(var.clone());
}
}
let mut queue: std::collections::VecDeque<String> = in_degree
.iter()
.filter(|(_, &d)| d == 0)
.map(|(v, _)| v.clone())
.collect();
let mut order: Vec<String> = Vec::with_capacity(self.entities.len());
while let Some(var) = queue.pop_front() {
order.push(var.clone());
if let Some(deps_of) = dependents.get(&var) {
for dep in deps_of {
let d = in_degree.get_mut(dep).unwrap();
*d -= 1;
if *d == 0 {
queue.push_back(dep.clone());
}
}
}
}
if order.len() != self.entities.len() {
let placed: HashSet<&str> = order.iter().map(String::as_str).collect();
let mut cyclic: Vec<&str> = self
.entities
.keys()
.map(String::as_str)
.filter(|v| !placed.contains(v))
.collect();
cyclic.sort();
anyhow::bail!(
"WAC plan has dependency cycles among: [{}]",
cyclic.join(", ")
);
}
Ok(order)
}
}
struct SpliceCtx<'a> {
composition: &'a CompositionGraph,
splits_path: &'a str,
shim_comps: &'a HashMap<usize, usize>,
}
#[derive(Default)]
struct SpliceAccumulators {
checked_middlewares: HashMap<String, BTreeMap<String, ExportInfo>>,
generated_adapters: Vec<GeneratedAdapter>,
target_has_sync_cache: HashMap<(String, String), bool>,
middleware_first_async_peer_cache: HashMap<String, Option<String>>,
}
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> {
log_shim_resolutions(&shim_comps);
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 ctx = SpliceCtx {
composition,
splits_path,
shim_comps: &shim_comps,
};
let mut accs = SpliceAccumulators::default();
let mut diagnostics: Vec<ContractResult> = vec![];
let mut any_rule_matched = false;
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, &ctx, &mut accs)?;
let before = apply_rule_before(rule, chain, &ctx, &mut accs)?;
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);
}
any_rule_matched |= any_full_match;
if !any_full_match {
let iface = rule.interface();
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 plan = EmitPlan::new()
.with_aliases(&chains)
.with_node_vars(composition, &chains, &shim_comps)
.with_chain_routing(&chains, composition, &shim_comps)?
.with_node_entities(composition, &shim_comps)?
.with_exports(composition, &chains, &handled_interfaces, &shim_comps);
let wac = plan.render(pkg_name)?;
let args = gen_wac_args(
shim_comps,
splits_path,
composition,
&plan.used_comp_nodes,
&plan.used_middlewares,
node_paths,
);
Ok(WacOutput {
wac,
wac_deps: args,
diagnostics,
generated_adapters: accs.generated_adapters,
any_rule_matched,
})
}
fn gen_wac_args(
shim_comps: HashMap<usize, usize>,
splits_path: &str,
graph: &CompositionGraph,
used_comps: &HashMap<u32, String>,
used_mdls: &BTreeMap<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 log_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 {
tracing::debug!(
"Assumption made: split{shim_num} appears to be a shim component, \
defaulting to split{resolved} in the generated wac command. \
If this is incorrect, modify the generated wac command."
);
}
}
}
struct RuleApplyResult {
contract_results: Vec<ContractResult>,
interface_matched: bool,
full_match: bool,
}
fn build_per_edge_providers(
inject: &[Injection],
edge_id: &str,
splits_path: &str,
) -> anyhow::Result<Vec<Injection>> {
let splits_dir = std::path::Path::new(splits_path);
let edge_suffix = sanitize_wac_id(edge_id);
inject
.iter()
.map(|inj| {
if inj.config_as_wave.is_none() {
return Ok(inj.clone());
}
let mut clone = inj.clone();
clone.name = format!("{}-{edge_suffix}", inj.name);
crate::config_provider::build_provider_for_edge(&mut clone, edge_id, splits_dir)?;
Ok(clone)
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn apply_rule_between(
rule: &SpliceRule,
chain: &mut Chain,
ctx: &SpliceCtx,
accs: &mut SpliceAccumulators,
) -> 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 = &ctx.composition.nodes[&inner_id];
let outer_node = &ctx.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 edge_id = derive_edge_id(interface, Some(&outer_var), &inner_var);
let edge_inject = build_per_edge_providers(inject, &edge_id, ctx.splits_path)?;
let new_aliases = vec![
(inner_id, inner_alias.clone()),
(outer_id, outer_alias.clone()),
];
let consumer_path = chain.consumer_split_path(
i + 1,
ctx.composition,
ctx.splits_path,
ctx.shim_comps,
);
contract_results.extend(add_to_inject_plan(
interface,
&edge_inject,
i + 1,
&new_aliases,
&mut chain.aliases,
&mut chain.inject_plan,
&chain.interface.ty_fingerprint,
consumer_path,
ctx,
accs,
)?);
}
}
}
Ok(RuleApplyResult {
contract_results,
interface_matched,
full_match,
})
}
fn apply_rule_before(
rule: &SpliceRule,
chain: &mut Chain,
ctx: &SpliceCtx,
accs: &mut SpliceAccumulators,
) -> 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 = &ctx.composition.nodes[id];
if let Some(provider) = provider_name {
if get_name(outer_node) != *provider {
continue;
}
}
full_match = true;
let provider_var = get_name(outer_node).to_string();
let caller_var = chain
.chain
.get(i + 1)
.map(|caller_id| get_name(&ctx.composition.nodes[caller_id]).to_string());
let edge_id = derive_edge_id(interface, caller_var.as_deref(), &provider_var);
let edge_inject = build_per_edge_providers(inject, &edge_id, ctx.splits_path)?;
let new_aliases = vec![(*id, provider_alias.clone())];
let consumer_path = chain
.consumer_split_path(i + 1, ctx.composition, ctx.splits_path, ctx.shim_comps)
.or_else(|| {
chain.consumer_split_path(i, ctx.composition, ctx.splits_path, ctx.shim_comps)
});
contract_results.extend(add_to_inject_plan(
interface,
&edge_inject,
i + 1,
&new_aliases,
&mut chain.aliases,
&mut chain.inject_plan,
&chain.interface.ty_fingerprint,
consumer_path,
ctx,
accs,
)?);
}
}
Ok(RuleApplyResult {
contract_results,
interface_matched,
full_match,
})
}
fn materialize_tier3_4_inline(
interface_name: &str,
to_inject: &[Injection],
consumer_split: Option<&str>,
ctx: &SpliceCtx,
) -> anyhow::Result<Vec<Injection>> {
use crate::strategies::Tier3_4Source;
let sources: Vec<Option<Tier3_4Source<'_>>> =
to_inject.iter().map(classify_tier3_4_source).collect();
if sources.iter().all(Option::is_none) {
return Ok(to_inject.to_vec());
}
let read_split = |label: &str| -> anyhow::Result<Vec<u8>> {
let split_path = consumer_split.ok_or_else(|| {
anyhow::anyhow!("no split for tier-3/4 '{label}' on '{interface_name}'")
})?;
std::fs::read(split_path)
.with_context(|| format!("read split for tier-3/4 codegen: {split_path}"))
};
let mut out: Vec<Injection> = Vec::with_capacity(to_inject.len());
for (inj, source) in to_inject.iter().zip(sources) {
let Some(source) = source else {
out.push(inj.clone());
continue;
};
let label = source_label(&source);
let split_bytes = read_split(label)?;
let (wrapper_path, tier) = crate::strategies::materialize_tier3_4(
std::path::Path::new(ctx.splits_path),
source,
&split_bytes,
interface_name,
)
.with_context(|| {
format!("materialize tier-3/4 strategy '{label}' on '{interface_name}'")
})?;
out.push(stamp_materialized(inj, wrapper_path, tier)?);
}
Ok(out)
}
fn classify_tier3_4_source(inj: &Injection) -> Option<crate::strategies::Tier3_4Source<'_>> {
use crate::strategies::Tier3_4Source;
if let Some(b) = inj.builtin.as_deref() {
if crate::strategies::is_embedded_builtin(b) {
return Some(Tier3_4Source::Builtin(b));
}
return None;
}
if let Some(p) = inj.path.as_deref() {
let dir = std::path::Path::new(p);
if crate::strategies::is_user_strategy_dir(dir) {
return Some(Tier3_4Source::User {
wac_name: &inj.name,
strategy_dir: dir,
});
}
}
None
}
fn source_label<'a>(source: &crate::strategies::Tier3_4Source<'a>) -> &'a str {
use crate::strategies::Tier3_4Source;
match source {
Tier3_4Source::Builtin(name) => name,
Tier3_4Source::User { wac_name, .. } => wac_name,
}
}
fn stamp_materialized(
inj: &Injection,
wrapper_path: std::path::PathBuf,
tier: builtin_protocol::Tier,
) -> anyhow::Result<Injection> {
let path_str = wrapper_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("wrapper path is not UTF-8: {}", wrapper_path.display()))?
.to_string();
Ok(Injection {
path: Some(path_str),
tier: Some(tier),
..inj.clone()
})
}
#[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>,
consumer_split: Option<String>,
ctx: &SpliceCtx,
accs: &mut SpliceAccumulators,
) -> anyhow::Result<Vec<ContractResult>> {
let to_inject_materialized =
materialize_tier3_4_inline(interface_name, to_inject, consumer_split.as_deref(), ctx)?;
let contract_results = validate_contract(
&to_inject_materialized,
interface_name,
contract_fingerprint,
&mut accs.checked_middlewares,
);
let mut resolved: Vec<Injection> = Vec::with_capacity(to_inject_materialized.len());
let mut final_results: Vec<ContractResult> = Vec::with_capacity(contract_results.len());
for (injection, result) in to_inject_materialized.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
)
})?;
if let Some(mw_path) = injection.path.as_deref() {
preflight_sync_target_async_middleware(
&injection.name,
mw_path,
interface_name,
consumer_split_path,
accs,
)?;
}
let adapter_path = generate_tier1_adapter(
&injection.name,
interface_name,
&matched_interfaces,
ctx.splits_path,
consumer_split_path,
)?;
accs.generated_adapters.push(GeneratedAdapter {
adapter_path: adapter_path.clone(),
middleware_name: injection.name.clone(),
target_interface: interface_name.to_string(),
matched_hook_interfaces: matched_interfaces.clone(),
});
resolved.push(Injection {
adapter_info: Some(AdapterInjectionInfo {
adapter_path,
matched_hook_interfaces: matched_interfaces,
}),
..injection.clone()
});
}
ContractResult::Tier2Compatible(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 tier-2 adapter for middleware '{}'.",
injection.name
)
})?;
if let Some(mw_path) = injection.path.as_deref() {
preflight_sync_target_async_middleware(
&injection.name,
mw_path,
interface_name,
consumer_split_path,
accs,
)?;
}
let adapter_path = generate_tier2_adapter(
&injection.name,
interface_name,
&matched_interfaces,
ctx.splits_path,
consumer_split_path,
)?;
accs.generated_adapters.push(GeneratedAdapter {
adapter_path: adapter_path.clone(),
middleware_name: injection.name.clone(),
target_interface: interface_name.to_string(),
matched_hook_interfaces: matched_interfaces.clone(),
});
resolved.push(Injection {
adapter_info: Some(AdapterInjectionInfo {
adapter_path,
matched_hook_interfaces: matched_interfaces,
}),
..injection.clone()
});
}
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)
}
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 preflight_sync_target_async_middleware(
middleware_name: &str,
middleware_path: &str,
target_interface: &str,
target_split_path: &str,
accs: &mut SpliceAccumulators,
) -> anyhow::Result<()> {
let target_key = (target_split_path.to_string(), target_interface.to_string());
let has_sync = *accs
.target_has_sync_cache
.entry(target_key)
.or_insert_with(|| target_interface_has_sync_func(target_interface, target_split_path));
if !has_sync {
return Ok(());
}
let offender = accs
.middleware_first_async_peer_cache
.entry(middleware_path.to_string())
.or_insert_with(|| first_async_peer_import(middleware_path))
.clone();
let Some(offender) = offender else {
return Ok(());
};
anyhow::bail!(
"Cannot splice middleware '{middleware_name}' onto SYNC-WIT target \
interface '{target_interface}': the middleware imports '{offender}', \
which is `async func` from a peer component. Awaiting a peer-component \
async call inside a hook body wedges the wasm task at runtime — splicer's \
generated adapter has to lift `{target_interface}` as sync-WIT (matching \
the target's contract), and sync-WIT-rooted tasks cannot suspend. \
Splice this middleware on an `async func` target interface, or rewrite \
the middleware to read its substrate inline (no `.await`). \
See docs/TODO/sync-wit-suspend-limit.md.",
);
}
fn target_interface_has_sync_func(target_interface: &str, target_split_path: &str) -> bool {
let Ok(bytes) = std::fs::read(target_split_path) else {
return false;
};
let Ok(decoded) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
wit_component::decode(&bytes)
})) else {
return false;
};
let Ok(wit_component::DecodedWasm::Component(resolve, world_id)) = decoded else {
return false;
};
let world = &resolve.worlds[world_id];
let surfaces = world.imports.values().chain(world.exports.values());
for item in surfaces {
let wit_parser::WorldItem::Interface { id, .. } = item else {
continue;
};
let Some(qname) = resolve.id_of(*id) else {
continue;
};
if qname.split('@').next().unwrap_or(&qname)
!= target_interface
.split('@')
.next()
.unwrap_or(target_interface)
{
continue;
}
let iface = &resolve.interfaces[*id];
if iface.functions.values().any(|f| !f.kind.is_async()) {
return true;
}
}
false
}
fn first_async_peer_import(middleware_path: &str) -> Option<String> {
let bytes = std::fs::read(middleware_path).ok()?;
let decoded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
wit_component::decode(&bytes)
}))
.ok()?
.ok()?;
let wit_component::DecodedWasm::Component(resolve, world_id) = decoded else {
return None;
};
let world = &resolve.worlds[world_id];
for (_key, item) in &world.imports {
let wit_parser::WorldItem::Interface { id, .. } = item else {
continue;
};
let Some(qname) = resolve.id_of(*id) else {
continue;
};
if qname.starts_with("wasi:") {
continue;
}
let iface = &resolve.interfaces[*id];
for (fn_name, func) in &iface.functions {
if func.kind.is_async() {
return Some(format!("{qname}#{fn_name}"));
}
}
}
None
}
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 disambiguated_var(counts: &mut HashMap<String, usize>, pkg: &str) -> String {
let count = counts.entry(pkg.to_string()).or_insert(0);
let var = if *count == 0 {
pkg.to_string()
} else {
format!("{pkg}-{count}")
};
*count += 1;
var
}
fn sanitize_wac_id(raw: &str) -> String {
let sanitized: String = raw
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let stripped = sanitized
.strip_prefix(&format!("{INST_PREFIX}-"))
.unwrap_or(&sanitized);
stripped
.split('-')
.filter(|s| !s.is_empty())
.map(|seg| match seg.chars().next() {
Some(c) if c.is_ascii_digit() => format!("v{seg}"),
_ => seg.to_string(),
})
.collect::<Vec<_>>()
.join("-")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edge_id_internal_renders_caller_to_provider() {
assert_eq!(
derive_edge_id(
"wasi:http/handler@0.3.0-rc-2026-01-06",
Some("srv-b"),
"srv-a",
),
"wasi:http/handler@0.3.0-rc-2026-01-06::srv-b->srv-a",
);
}
#[test]
fn edge_id_boundary_drops_caller_segment() {
assert_eq!(
derive_edge_id("wasi:http/handler@0.3.0", None, "srv-a"),
"wasi:http/handler@0.3.0::->srv-a",
);
}
#[test]
fn edge_id_before_and_between_targeting_same_edge_collide() {
let from_between = derive_edge_id("ns:pkg/iface@1.0.0", Some("A"), "B");
let from_before = derive_edge_id("ns:pkg/iface@1.0.0", Some("A"), "B");
assert_eq!(from_between, from_before);
}
#[test]
fn edge_id_derivation_is_deterministic() {
let a = derive_edge_id("ns:pkg/iface@1.2.3", Some("caller"), "provider");
let b = derive_edge_id("ns:pkg/iface@1.2.3", Some("caller"), "provider");
assert_eq!(a, b);
}
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}"
);
}
fn write_minimal_component(name: &str) -> String {
let bytes = wat::parse_str("(component)").expect("compile minimal component");
let path =
std::env::temp_dir().join(format!("splicer-test-{}-{}.wasm", name, std::process::id()));
std::fs::write(&path, bytes).expect("write tempfile");
path.to_string_lossy().into_owned()
}
#[test]
fn config_provider_wired_when_path_set() {
use crate::parse::config::{AdapterInjectionInfo, Injection};
let cfg_path = write_minimal_component("metrics-config");
let adapter_path = write_minimal_component("metrics-adapter");
let mdl = Injection {
name: "metrics".to_string(),
path: Some("/tmp/metrics.wasm".to_string()),
builtin: Some("otel-bare-metrics".to_string()),
builtin_config: Default::default(),
config_as_wave: None,
config_provider_path: Some(cfg_path.clone()),
adapter_info: Some(AdapterInjectionInfo {
adapter_path,
matched_hook_interfaces: vec!["splicer:tier1/before".to_string()],
}),
tier: None,
};
let contract = Contract {
name: "wasi:http/handler@0.3.0".to_string(),
ty_fingerprint: None,
};
let graph = synth_graph(1, &[]);
let mut plan = EmitPlan::new();
let _adapter_var = plan
.add_middleware(&mdl, &contract, "downstream", &graph, &HashMap::new())
.expect("plan");
let cfg = plan
.entities
.get("metrics-config")
.expect("config provider entity must be added");
assert!(
cfg.imports.is_empty() && cfg.catchall,
"config provider should be `{{ ... }}` form; got: {cfg:?}"
);
let real = plan
.entities
.get("metrics")
.expect("real middleware entity must be added");
assert!(
real.imports
.iter()
.any(|(iface, src)| iface == "splicer:builtin-config/get@0.1.0"
&& src == "metrics-config"),
"real middleware must wire the provider's `get` export; got: {real:?}"
);
assert!(
plan.used_middlewares
.iter()
.any(|(name, path)| name == "metrics-config" && path == &cfg_path),
"config provider must be registered in used_middlewares; got: {:?}",
plan.used_middlewares
);
}
#[test]
fn config_provider_not_wired_when_path_unset() {
use crate::parse::config::{AdapterInjectionInfo, Injection};
let adapter_path = write_minimal_component("hello-adapter");
let mdl = Injection {
name: "hello".to_string(),
path: Some("/tmp/hello.wasm".to_string()),
builtin: Some("hello-tier1".to_string()),
builtin_config: Default::default(),
config_as_wave: None,
config_provider_path: None,
adapter_info: Some(AdapterInjectionInfo {
adapter_path,
matched_hook_interfaces: vec!["splicer:tier1/before".to_string()],
}),
tier: None,
};
let contract = Contract {
name: "wasi:http/handler@0.3.0".to_string(),
ty_fingerprint: None,
};
let graph = synth_graph(1, &[]);
let mut plan = EmitPlan::new();
let _adapter_var = plan
.add_middleware(&mdl, &contract, "downstream", &graph, &HashMap::new())
.expect("plan");
let real = plan
.entities
.get("hello")
.expect("real middleware entity must be added");
assert!(
real.imports.is_empty() && real.catchall,
"real middleware should be `{{ ... }}` form; got: {real:?}"
);
assert!(
!plan.entities.contains_key("hello-config"),
"no config provider entity should be added; got entities: {:?}",
plan.entities.keys().collect::<Vec<_>>()
);
assert!(
plan.used_middlewares
.iter()
.all(|(name, _)| name != "hello-config"),
"no config provider should appear in used_middlewares; got: {:?}",
plan.used_middlewares
);
}
#[test]
fn middle_node_consumer_import_routes_through_inner_middleware() {
use crate::parse::config::{Injection, SpliceRule};
use cviz::model::{ComponentNode, CompositionGraph, InterfaceConnection};
let mut graph = CompositionGraph::new();
graph.add_node(0, ComponentNode::new("$inner".to_string(), 0, 0));
let mut middle = ComponentNode::new("$middle".to_string(), 1, 1);
middle.add_import(InterfaceConnection {
interface_name: "test:demo/inner".to_string(),
source_instance: Some(0),
is_host_import: false,
fingerprint: None,
interface_type: None,
});
graph.add_node(1, middle);
let mut outer = ComponentNode::new("$outer".to_string(), 2, 2);
outer.add_import(InterfaceConnection {
interface_name: "test:demo/middle".to_string(),
source_instance: Some(1),
is_host_import: false,
fingerprint: None,
interface_type: None,
});
graph.add_node(2, outer);
graph.add_export("test:demo/outer".to_string(), 2, None);
let rules = vec![SpliceRule::Before {
interface: "test:demo/inner".to_string(),
provider_name: Some("inner".to_string()),
provider_alias: None,
inject: vec![Injection {
name: "mw".to_string(),
path: None,
builtin: None,
builtin_config: Default::default(),
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}],
}];
let out = generate_wac(
HashMap::new(),
"/tmp/splicer-test-splits",
&graph,
&rules,
None,
"test:nested",
)
.expect("generate_wac");
assert!(
out.wac.contains("let mw = new my:mw {"),
"mw middleware var must be emitted; got:\n{}",
out.wac
);
let start = out
.wac
.find("let middle = new my:middle")
.expect("middle instance must be emitted");
let end = out.wac[start..]
.find("};")
.expect("middle block must close");
let middle_block = &out.wac[start..start + end];
assert!(
middle_block.contains("\"test:demo/inner\": mw[\"test:demo/inner\"]"),
"middle must wire `test:demo/inner` through the injected \
`mw` middleware (the inner `before` rule), but it doesn't. \
middle block:\n{middle_block}\n\nFull WAC:\n{}",
out.wac
);
}
}