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            let wire_types = program.node_wire_types(i);
94            residue.push(ResidueEntry {
95                name: node.meta().name.clone(),
96                p3_classifiable: p3_classifiable(node),
97                // Every compiled form, not only the scalar one: a node
98                // whose form is a slot kit is P2-capable too.
99                p2_capable: crate::compile::node_tier(node, &wire_types)
100                    != crate::ast::CompileLevel::Phase1,
101            });
102        }
103    }
104    let p2_headroom = residue
105        .iter()
106        .filter(|r| r.p2_capable && !r.p3_classifiable)
107        .count();
108    let p3_unfused = residue.iter().filter(|r| r.p3_classifiable).count();
109    LatticeReport {
110        cones,
111        fused_nodes,
112        residue,
113        p2_headroom,
114        p3_unfused,
115    }
116}
117
118impl std::fmt::Display for LatticeReport {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        writeln!(
121            f,
122            "lattice: {} cone{} ({} node{} fused), {} interpreter node{}",
123            self.cones.len(),
124            if self.cones.len() == 1 { "" } else { "s" },
125            self.fused_nodes,
126            if self.fused_nodes == 1 { "" } else { "s" },
127            self.residue.len(),
128            if self.residue.len() == 1 { "" } else { "s" },
129        )?;
130        for cone in &self.cones {
131            writeln!(
132                f,
133                "  cone {} — {} member{}, boundary {}→{}",
134                cone.label,
135                cone.members.len(),
136                if cone.members.len() == 1 { "" } else { "s" },
137                cone.boundary_in,
138                cone.boundary_out,
139            )?;
140        }
141        for r in &self.residue {
142            let tier = match (r.p3_classifiable, r.p2_capable) {
143                (true, _) => "p3-classifiable, unfused",
144                (false, true) => "p2-capable (headroom)",
145                (false, false) => "p1-only",
146            };
147            writeln!(f, "  interp {:30} [{tier}]", r.name)?;
148        }
149        if self.p2_headroom > 0 {
150            writeln!(
151                f,
152                "  headroom: {} node{} p2-capable without p3 — candidates \
153                 for P2-at-cone-boundaries (SRD-105)",
154                self.p2_headroom,
155                if self.p2_headroom == 1 { "" } else { "s" },
156            )?;
157        }
158        Ok(())
159    }
160}