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