Skip to main content

polydat_core/compile/
lattice.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Interpreter cone lattice report — what extraction actually did
5//! (engines.md §2).
6//!
7//! Walks a compiled program and reports its engine mix: which
8//! cones formed (members, boundary shape), which nodes stayed on
9//! the interpreter, and the lattice headroom per node — P3
10//! classifiability and whether the node has a compiled P2 form (a
11//! `compiled_u64` op or a slot kit). The headroom column counts the
12//! candidates for running P2 closures at cone boundaries: nodes that
13//! are P2-capable but not JIT-classifiable run P1 dyn dispatch on
14//! the interpreter.
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 has a compiled P2 form, a `compiled_u64` op or a slot
40    /// kit, so the P2 middle rung could run it even though P3 can't.
41    pub p2_capable: bool,
42}
43
44/// Engine-mix report for one compiled program.
45pub struct LatticeReport {
46    /// The fused cones, in program order.
47    pub cones: Vec<ConeEntry>,
48    /// Total nodes fused into cones (sum of members).
49    pub fused_nodes: usize,
50    /// The nodes left on the interpreter, in program order.
51    pub residue: Vec<ResidueEntry>,
52    /// Residue nodes with a compiled P2 form but no P3
53    /// classification — the "P2 closures at cone boundaries"
54    /// candidate set.
55    pub p2_headroom: usize,
56    /// Residue nodes the P3 classifier CAN lower that still ended
57    /// up interpreted (const/scope-init lifecycle, threshold,
58    /// boundary types).
59    pub p3_unfused: usize,
60}
61
62#[cfg(feature = "jit")]
63fn p3_classifiable(node: &dyn crate::ast::PolydatNode) -> bool {
64    !matches!(
65        crate::compile::jit::classify_node(node),
66        crate::compile::jit::JitOp::Fallback
67    )
68}
69
70#[cfg(not(feature = "jit"))]
71fn p3_classifiable(_node: &dyn crate::ast::PolydatNode) -> bool {
72    false
73}
74
75/// Walk `program` and report its engine mix.
76pub fn lattice_report(program: &PolydatProgram) -> LatticeReport {
77    let mut cones = Vec::new();
78    let mut residue = Vec::new();
79    let mut fused_nodes = 0;
80    for i in 0..program.node_count() {
81        let node = program.node_ref(i);
82        if let Some(sub) = node.fusion_subgraph() {
83            let members: Vec<String> = sub.members.iter().map(|m| m.meta().name.clone()).collect();
84            fused_nodes += members.len();
85            cones.push(ConeEntry {
86                label: node.meta().name.clone(),
87                members,
88                boundary_in: node.meta().wire_inputs().len(),
89                boundary_out: node.meta().outs.len(),
90            });
91        } else {
92            let wire_types = program.node_wire_types(i);
93            residue.push(ResidueEntry {
94                name: node.meta().name.clone(),
95                p3_classifiable: p3_classifiable(node),
96                // Every compiled form, not only the scalar one: a node
97                // whose form is a slot kit is P2-capable too.
98                p2_capable: crate::compile::node_tier(node, &wire_types)
99                    != crate::ast::CompileLevel::Phase1,
100            });
101        }
102    }
103    let p2_headroom = residue
104        .iter()
105        .filter(|r| r.p2_capable && !r.p3_classifiable)
106        .count();
107    let p3_unfused = residue.iter().filter(|r| r.p3_classifiable).count();
108    LatticeReport {
109        cones,
110        fused_nodes,
111        residue,
112        p2_headroom,
113        p3_unfused,
114    }
115}
116
117impl std::fmt::Display for LatticeReport {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        writeln!(
120            f,
121            "lattice: {} cone{} ({} node{} fused), {} interpreter node{}",
122            self.cones.len(),
123            if self.cones.len() == 1 { "" } else { "s" },
124            self.fused_nodes,
125            if self.fused_nodes == 1 { "" } else { "s" },
126            self.residue.len(),
127            if self.residue.len() == 1 { "" } else { "s" },
128        )?;
129        for cone in &self.cones {
130            writeln!(
131                f,
132                "  cone {} — {} member{}, boundary {}→{}",
133                cone.label,
134                cone.members.len(),
135                if cone.members.len() == 1 { "" } else { "s" },
136                cone.boundary_in,
137                cone.boundary_out,
138            )?;
139        }
140        for r in &self.residue {
141            let tier = match (r.p3_classifiable, r.p2_capable) {
142                (true, _) => "p3-classifiable, unfused",
143                (false, true) => "p2-capable (headroom)",
144                (false, false) => "p1-only",
145            };
146            writeln!(f, "  interp {:30} [{tier}]", r.name)?;
147        }
148        if self.p2_headroom > 0 {
149            writeln!(
150                f,
151                "  headroom: {} node{} p2-capable without p3 — candidates \
152                 for P2-at-cone-boundaries (engines.md §2)",
153                self.p2_headroom,
154                if self.p2_headroom == 1 { "" } else { "s" },
155            )?;
156        }
157        Ok(())
158    }
159}