use crate::kernel::PolydatProgram;
pub struct ConeEntry {
pub label: String,
pub members: Vec<String>,
pub boundary_in: usize,
pub boundary_out: usize,
}
pub struct ResidueEntry {
pub name: String,
pub p3_classifiable: bool,
pub p2_capable: bool,
}
pub struct LatticeReport {
pub cones: Vec<ConeEntry>,
pub fused_nodes: usize,
pub residue: Vec<ResidueEntry>,
pub p2_headroom: usize,
pub p3_unfused: usize,
}
#[cfg(feature = "jit")]
fn p3_classifiable(node: &dyn crate::ast::PolydatNode) -> bool {
!matches!(
crate::compile::jit::classify_node(node),
crate::compile::jit::JitOp::Fallback
)
}
#[cfg(not(feature = "jit"))]
fn p3_classifiable(_node: &dyn crate::ast::PolydatNode) -> bool {
false
}
pub fn lattice_report(program: &PolydatProgram) -> LatticeReport {
let mut cones = Vec::new();
let mut residue = Vec::new();
let mut fused_nodes = 0;
for i in 0..program.node_count() {
let node = program.node_ref(i);
if let Some(sub) = node.fusion_subgraph() {
let members: Vec<String> = sub
.members
.iter()
.map(|m| m.meta().name.clone())
.collect();
fused_nodes += members.len();
cones.push(ConeEntry {
label: node.meta().name.clone(),
members,
boundary_in: node.meta().wire_inputs().len(),
boundary_out: node.meta().outs.len(),
});
} else {
residue.push(ResidueEntry {
name: node.meta().name.clone(),
p3_classifiable: p3_classifiable(node),
p2_capable: node.compiled_u64().is_some(),
});
}
}
let p2_headroom = residue
.iter()
.filter(|r| r.p2_capable && !r.p3_classifiable)
.count();
let p3_unfused = residue.iter().filter(|r| r.p3_classifiable).count();
LatticeReport {
cones,
fused_nodes,
residue,
p2_headroom,
p3_unfused,
}
}
impl std::fmt::Display for LatticeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"lattice: {} cone{} ({} node{} fused), {} interpreter node{}",
self.cones.len(),
if self.cones.len() == 1 { "" } else { "s" },
self.fused_nodes,
if self.fused_nodes == 1 { "" } else { "s" },
self.residue.len(),
if self.residue.len() == 1 { "" } else { "s" },
)?;
for cone in &self.cones {
writeln!(
f,
" cone {} — {} member{}, boundary {}→{}",
cone.label,
cone.members.len(),
if cone.members.len() == 1 { "" } else { "s" },
cone.boundary_in,
cone.boundary_out,
)?;
}
for r in &self.residue {
let tier = match (r.p3_classifiable, r.p2_capable) {
(true, _) => "p3-classifiable, unfused",
(false, true) => "p2-capable (headroom)",
(false, false) => "p1-only",
};
writeln!(f, " interp {:30} [{tier}]", r.name)?;
}
if self.p2_headroom > 0 {
writeln!(
f,
" headroom: {} node{} p2-capable without p3 — candidates \
for P2-at-cone-boundaries (SRD-105)",
self.p2_headroom,
if self.p2_headroom == 1 { "" } else { "s" },
)?;
}
Ok(())
}
}
#[cfg(all(test, feature = "jit"))]
mod tests {
use super::*;
use crate::compile::cone::JitMode;
use crate::dsl::compile::compile_polydat_to_assembler;
fn report_for(src: &str, mode: JitMode) -> LatticeReport {
let mut asm = compile_polydat_to_assembler(src).expect("assemble");
asm.set_jit_mode(mode);
let k = asm.compile().expect("compile");
lattice_report(k.program())
}
#[test]
fn mixed_graph_reports_cone_and_residue() {
let src = "input (x: u64)\n\
v := mul(x, 3)\n\
w := add(v, 7)\n\
out := default_or(w, 9)\n";
let rep = report_for(src, JitMode::Auto);
assert_eq!(rep.cones.len(), 1, "one cone expected");
assert_eq!(rep.fused_nodes, 2, "mul+add fused");
assert!(
rep.cones[0].members.contains(&"mul".to_string())
&& rep.cones[0].members.contains(&"add".to_string()),
"members listed: {:?}",
rep.cones[0].members
);
assert!(
rep.residue.iter().any(|r| r.name == "default_or"),
"fallback node in residue"
);
let shown = format!("{rep}");
assert!(shown.contains("jit_cone["), "display names the cone: {shown}");
}
#[test]
fn off_mode_reports_pure_residue() {
let src = "input (x: u64)\n\
v := mul(x, 3)\n\
w := add(v, 7)\n";
let rep = report_for(src, JitMode::Off);
assert!(rep.cones.is_empty());
assert_eq!(rep.fused_nodes, 0);
assert!(rep.p3_unfused >= 2, "p3_unfused: {}", rep.p3_unfused);
}
}