Skip to main content

polydat_core/compile/
lattice.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! SRD-105 lattice report — what extraction actually did.
5//!
6//! Walks a compiled program and reports its engine mix: which
7//! cones formed (members, boundary shape), which nodes stayed on
8//! the interpreter, and the lattice headroom per node — P3
9//! classifiability and P2 (`compiled_u64`) capability. The
10//! headroom column is the standing evidence feed for the parked
11//! "P2 closures at cone boundaries" extension (SRD-105 §Rejected
12//! alternatives addendum): nodes that are u64-capable but not
13//! JIT-classifiable currently run P1 dyn dispatch.
14//!
15//! Rendered by `nbrs bench wiring <expr> --cones`.
16
17use crate::kernel::PolydatProgram;
18
19/// One fused cone in the compiled program.
20pub struct ConeEntry {
21    /// The cone node's diagnostic label (`jit_cone[…]`).
22    pub label: String,
23    /// Member function names, in fusion order.
24    pub members: Vec<String>,
25    /// Boundary inputs the cone reads.
26    pub boundary_in: usize,
27    /// Boundary outputs it writes.
28    pub boundary_out: usize,
29}
30
31/// One node left on the interpreter.
32pub struct ResidueEntry {
33    /// The node's function name.
34    pub name: String,
35    /// The P3 classifier can lower this node (it stayed unfused
36    /// for lifecycle / threshold / boundary reasons).
37    pub p3_classifiable: bool,
38    /// The node carries a `compiled_u64` closure — the P2 middle
39    /// rung could run it even though P3 can't.
40    pub p2_capable: bool,
41}
42
43/// Engine-mix report for one compiled program.
44pub struct LatticeReport {
45    /// The fused cones, in program order.
46    pub cones: Vec<ConeEntry>,
47    /// Total nodes fused into cones (sum of members).
48    pub fused_nodes: usize,
49    /// The nodes left on the interpreter, in program order.
50    pub residue: Vec<ResidueEntry>,
51    /// Residue nodes with a P2 closure but no P3 classification —
52    /// the "P2 closures at cone boundaries" candidate set.
53    pub p2_headroom: usize,
54    /// Residue nodes the P3 classifier CAN lower that still ended
55    /// up interpreted (const/scope-init lifecycle, threshold,
56    /// boundary types).
57    pub p3_unfused: usize,
58}
59
60#[cfg(feature = "jit")]
61fn p3_classifiable(node: &dyn crate::ast::PolydatNode) -> bool {
62    !matches!(
63        crate::compile::jit::classify_node(node),
64        crate::compile::jit::JitOp::Fallback
65    )
66}
67
68#[cfg(not(feature = "jit"))]
69fn p3_classifiable(_node: &dyn crate::ast::PolydatNode) -> bool {
70    false
71}
72
73/// Walk `program` and report its engine mix.
74pub fn lattice_report(program: &PolydatProgram) -> LatticeReport {
75    let mut cones = Vec::new();
76    let mut residue = Vec::new();
77    let mut fused_nodes = 0;
78    for i in 0..program.node_count() {
79        let node = program.node_ref(i);
80        if let Some(sub) = node.fusion_subgraph() {
81            let members: Vec<String> = sub.members.iter().map(|m| m.meta().name.clone()).collect();
82            fused_nodes += members.len();
83            cones.push(ConeEntry {
84                label: node.meta().name.clone(),
85                members,
86                boundary_in: node.meta().wire_inputs().len(),
87                boundary_out: node.meta().outs.len(),
88            });
89        } else {
90            residue.push(ResidueEntry {
91                name: node.meta().name.clone(),
92                p3_classifiable: p3_classifiable(node),
93                p2_capable: node.compiled_u64().is_some(),
94            });
95        }
96    }
97    let p2_headroom = residue
98        .iter()
99        .filter(|r| r.p2_capable && !r.p3_classifiable)
100        .count();
101    let p3_unfused = residue.iter().filter(|r| r.p3_classifiable).count();
102    LatticeReport {
103        cones,
104        fused_nodes,
105        residue,
106        p2_headroom,
107        p3_unfused,
108    }
109}
110
111impl std::fmt::Display for LatticeReport {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        writeln!(
114            f,
115            "lattice: {} cone{} ({} node{} fused), {} interpreter node{}",
116            self.cones.len(),
117            if self.cones.len() == 1 { "" } else { "s" },
118            self.fused_nodes,
119            if self.fused_nodes == 1 { "" } else { "s" },
120            self.residue.len(),
121            if self.residue.len() == 1 { "" } else { "s" },
122        )?;
123        for cone in &self.cones {
124            writeln!(
125                f,
126                "  cone {} — {} member{}, boundary {}→{}",
127                cone.label,
128                cone.members.len(),
129                if cone.members.len() == 1 { "" } else { "s" },
130                cone.boundary_in,
131                cone.boundary_out,
132            )?;
133        }
134        for r in &self.residue {
135            let tier = match (r.p3_classifiable, r.p2_capable) {
136                (true, _) => "p3-classifiable, unfused",
137                (false, true) => "p2-capable (headroom)",
138                (false, false) => "p1-only",
139            };
140            writeln!(f, "  interp {:30} [{tier}]", r.name)?;
141        }
142        if self.p2_headroom > 0 {
143            writeln!(
144                f,
145                "  headroom: {} node{} p2-capable without p3 — candidates \
146                 for P2-at-cone-boundaries (SRD-105)",
147                self.p2_headroom,
148                if self.p2_headroom == 1 { "" } else { "s" },
149            )?;
150        }
151        Ok(())
152    }
153}