use alloc::vec::Vec;
use miden_ace_codegen::{
AceCircuit, AceConfig, AceDag, AceError, DagBuilder, InputKey, NodeId, NodeKind,
build_ace_dag_for_air,
};
use miden_core::{Felt, field::ExtensionField};
use miden_crypto::{
field::Algebra,
stark::air::{BaseAir, LiftedAir, symbolic::SymbolicExpressionExt},
};
use crate::MidenAir;
pub fn build_multi_air_ace_circuit<EF>(config: AceConfig) -> Result<AceCircuit<EF>, AceError>
where
EF: ExtensionField<Felt>,
SymbolicExpressionExt<Felt, EF>: Algebra<EF>,
{
use miden_ace_codegen::{InputCounts, InputLayout};
let airs = [MidenAir::CORE, MidenAir::CHIPLETS];
assert_eq!(
config.num_airs,
airs.len(),
"build_multi_air_ace_circuit builds the {}-AIR VM circuit; AceConfig::num_airs must match",
airs.len(),
);
const LMCS_ALIGNMENT: usize = 8;
struct AirParts<EF> {
dag: AceDag<EF>,
aligned_main: usize,
aligned_aux_coord: usize,
aux_n: usize,
num_periodic: usize,
}
let sub_config = AceConfig { num_airs: 1, ..config };
let mut parts: Vec<AirParts<EF>> = Vec::with_capacity(airs.len());
for air in &airs {
let artifacts = build_ace_dag_for_air::<MidenAir, Felt, EF>(air, sub_config)?;
let main_w = <MidenAir as BaseAir<Felt>>::width(air);
let aux_w = <MidenAir as LiftedAir<Felt, EF>>::aux_width(air);
parts.push(AirParts {
dag: artifacts.dag,
aligned_main: main_w.next_multiple_of(LMCS_ALIGNMENT),
aligned_aux_coord: (aux_w * miden_ace_codegen::EXT_DEGREE)
.next_multiple_of(LMCS_ALIGNMENT),
aux_n: <MidenAir as LiftedAir<Felt, EF>>::num_aux_values(air),
num_periodic: artifacts.layout.counts.num_periodic,
});
}
let num_public = <MidenAir as BaseAir<Felt>>::num_public_values(&airs[0]);
let combined_aux_coord_w: usize = parts.iter().map(|p| p.aligned_aux_coord).sum();
assert!(
combined_aux_coord_w.is_multiple_of(miden_ace_codegen::EXT_DEGREE),
"combined aux coord width must be even"
);
let combined_counts = InputCounts {
width: parts.iter().map(|p| p.aligned_main).sum(),
aux_width: combined_aux_coord_w / miden_ace_codegen::EXT_DEGREE,
num_aux_boundary: parts.iter().map(|p| p.aux_n).sum(),
num_public,
num_randomness: 2,
num_periodic: parts.iter().map(|p| p.num_periodic).max().unwrap_or(0),
num_quotient_chunks: config.num_quotient_chunks,
};
let check_public = |index: usize| -> InputKey {
assert!(
index < num_public,
"constraint references public value {index} outside the {num_public}-felt window",
);
InputKey::Public(index)
};
let combined_layout = match config.layout {
miden_ace_codegen::LayoutKind::Native => {
InputLayout::new_multi_air(combined_counts, config.num_airs)
},
miden_ace_codegen::LayoutKind::Masm => {
InputLayout::new_masm_multi_air(combined_counts, config.num_airs)
},
};
let mut builder = DagBuilder::<EF>::new();
let mut main_offset = 0usize;
let mut aux_w_offset = 0usize;
let mut boundary_offset = 0usize;
let mut accs: Vec<NodeId> = Vec::with_capacity(parts.len());
let mut shared_qv: Option<NodeId> = None;
for (air_index, part) in parts.iter().enumerate() {
let root_old = part.dag.root();
let translation = reemit_dag_with_rewrite(
&mut builder,
&part.dag,
|key| match key {
InputKey::Main { offset, index } => {
InputKey::Main { offset, index: index + main_offset }
},
InputKey::AuxCoord { offset, index, coord } => InputKey::AuxCoord {
offset,
index: index + aux_w_offset,
coord,
},
InputKey::AuxBusBoundary(slot) => InputKey::AuxBusBoundary(slot + boundary_offset),
InputKey::IsFirst => InputKey::IsFirstAir(air_index),
InputKey::IsLast => InputKey::IsLastAir(air_index),
InputKey::IsTransition => InputKey::IsTransitionAir(air_index),
InputKey::Public(i) => check_public(i),
other => other,
},
true, );
let (acc, qv) = match part.dag.nodes[root_old.index()] {
NodeKind::Sub(acc_id, qv_id) => {
(translation[acc_id.index()], translation[qv_id.index()])
},
_ => panic!("per-AIR sub-DAG root must be `Sub(acc, q*v)`"),
};
match shared_qv {
None => shared_qv = Some(qv),
Some(expected) if expected != qv => {
return Err(AceError::InvalidInputLayout {
message: "per-AIR quotient bindings must share the same q*v node".into(),
});
},
Some(_) => {},
}
accs.push(acc);
main_offset += part.aligned_main;
aux_w_offset += part.aligned_aux_coord / miden_ace_codegen::EXT_DEGREE;
boundary_offset += part.aux_n;
}
let shared_qv = shared_qv.expect("multi-AIR circuit requires at least one AIR");
let mabs: Vec<NodeId> =
(0..accs.len()).map(|i| builder.input(InputKey::MultiAirBeta(i))).collect();
let mut combined_acc: Option<NodeId> = None;
for (mab, acc) in mabs.iter().zip(&accs) {
let term = builder.mul(*mab, *acc);
combined_acc = Some(match combined_acc {
None => term,
Some(prev) => builder.add(prev, term),
});
}
let combined_acc = combined_acc.expect("multi-AIR circuit requires at least one AIR");
let combined_constraint = builder.sub(combined_acc, shared_qv);
let combined_dag = builder.build(combined_constraint);
miden_ace_codegen::emit_circuit(&combined_dag, combined_layout)
}
fn reemit_dag_with_rewrite<EF, F>(
builder: &mut DagBuilder<EF>,
source: &AceDag<EF>,
rewrite: F,
skip_root: bool,
) -> Vec<NodeId>
where
EF: ExtensionField<Felt>,
F: Fn(InputKey) -> InputKey,
{
let nodes = &source.nodes;
let limit = if skip_root && !nodes.is_empty() {
nodes.len() - 1
} else {
nodes.len()
};
let mut translation: Vec<NodeId> = Vec::with_capacity(nodes.len());
for node in nodes.iter().take(limit) {
let new_id = match *node {
NodeKind::Input(key) => builder.input(rewrite(key)),
NodeKind::Constant(v) => builder.constant(v),
NodeKind::Add(a, b) => builder.add(translation[a.index()], translation[b.index()]),
NodeKind::Sub(a, b) => builder.sub(translation[a.index()], translation[b.index()]),
NodeKind::Mul(a, b) => builder.mul(translation[a.index()], translation[b.index()]),
NodeKind::Neg(a) => builder.neg(translation[a.index()]),
};
translation.push(new_id);
}
translation
}