metrale_circuit/
render.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The stable text rendering of a plan: the golden files under
4//! `kernels/circuits/plans/` and `met circuit show`.
5//!
6//! Owner: metrale-circuit.
7//! Invariants:
8//! - The text is a pure function of its inputs, ASCII only, one group per line, with no
9//!   widths that depend on terminal size, so a diff of two renderings is a diff of plans.
10//! - Every in-plan edge appears exactly once: as a fused edge of the group it lives in, as a
11//!   written output of the group that stores it, or as a plan input.
12
13use std::fmt::Write as _;
14
15use crate::fuser::{EdgeState, FusionPlan, Group, Policy};
16use crate::ir::{Circuit, NodeIdx};
17use crate::rules::Numerics;
18use crate::runtime::RuntimeRoute;
19
20/// 2026-09-28: `key: value` lines printed under the title, in order (recipe, checkpoint,
21/// policy). The caller decides what they say; the renderer only prints them.
22pub type Header = Vec<(String, String)>;
23
24/// 2026-09-30: `header` with its settings line rebuilt from `policy` (after a device class
25/// re-reads its settings, or for a runtime route's arm).
26pub fn with_settings(header: &Header, policy: &Policy) -> Header {
27    header
28        .iter()
29        .map(|(k, v)| {
30            if k == "settings" {
31                let s: Vec<String> = policy
32                    .settings
33                    .iter()
34                    .map(|(k, v)| format!("{k}={v}"))
35                    .collect();
36                (k.clone(), s.join(" "))
37            } else {
38                (k.clone(), v.clone())
39            }
40        })
41        .collect()
42}
43
44/// 2026-10-02: What a rendering adds to a plan: `compute=<group>` on each group line (the compute
45/// unit its kernels run on, `venn::compute`), and under it one line per member node (its
46/// pipeline, `crate::pipeline`).
47pub struct Notes<'a> {
48    /// 2026-10-02: The group line's `compute=` value, where it answers.
49    pub group: &'a dyn Fn(&Group) -> Option<String>,
50    /// 2026-10-02: A member node's line, where it answers.
51    pub node: &'a dyn Fn(NodeIdx) -> Option<String>,
52}
53
54impl Notes<'_> {
55    /// 2026-10-02: Nothing added.
56    pub const NONE: Notes<'static> = Notes {
57        group: &|_| None,
58        node: &|_| None,
59    };
60}
61
62/// 2026-09-30: A runtime route's arm, rendered after the primary plan: a heading that names the
63/// route, its condition and the settings it plans as, then the arm's plan under `header`.
64/// 2026-10-02: With `notes` ([`render_noted`]).
65pub fn route_section(
66    circuit: &Circuit,
67    route: &RuntimeRoute,
68    plan: &FusionPlan,
69    header: &Header,
70    notes: &Notes<'_>,
71) -> String {
72    format!(
73        "\n# runtime route `{}`: when {}; planned as `{}` ({})\n\n{}",
74        route.id,
75        route.why,
76        route.plans_as_text(),
77        route.cite,
78        render_noted(circuit, plan, header, notes)
79    )
80}
81
82/// 2026-09-28: Render `plan` of `circuit`.
83pub fn render(circuit: &Circuit, plan: &FusionPlan, header: &Header) -> String {
84    render_noted(circuit, plan, header, &Notes::NONE)
85}
86
87/// 2026-10-02: [`render`], with `notes.group(group)` appended to each group's line as
88/// `compute=<note>` where it answers, and under it `  <node>: <notes.node(node)>` for each member
89/// node where that answers.
90pub fn render_noted(
91    circuit: &Circuit,
92    plan: &FusionPlan,
93    header: &Header,
94    notes: &Notes<'_>,
95) -> String {
96    let mut s = String::new();
97    let _ = writeln!(
98        s,
99        "# circuit plan: {} {} n={}",
100        plan.arch,
101        plan.mode.name(),
102        plan.rows
103    );
104    for (k, v) in header {
105        let _ = writeln!(s, "# {k}: {v}");
106    }
107    if let Some(t) = &plan.table {
108        let _ = writeln!(s, "# row table: {t}");
109    }
110    let fused = plan
111        .edge_states
112        .iter()
113        .filter(|s| matches!(s, Some(EdgeState::Fused(_))))
114        .count();
115    let materialized = plan
116        .edge_states
117        .iter()
118        .filter(|s| **s == Some(EdgeState::Materialized))
119        .count();
120    let _ = writeln!(s, "digest: {}", plan.digest);
121    let _ = writeln!(
122        s,
123        "groups: {}  launches: {}{}  edges: {} fused, {} materialized",
124        plan.groups.len(),
125        plan.launches(),
126        match plan.copies() {
127            0 => String::new(),
128            c => format!("  copies: {c}"),
129        },
130        fused,
131        materialized
132    );
133    let inputs: Vec<String> = circuit
134        .edges
135        .iter()
136        .enumerate()
137        .filter(|(e, edge)| {
138            plan.edge_states[*e].is_some()
139                && edge
140                    .producer
141                    .is_none_or(|p| !plan.groups.iter().any(|g| g.nodes.contains(&p)))
142        })
143        .map(|(e, edge)| format!("{} {}", edge.id, plan.edge_formats[e]))
144        .collect();
145    if !inputs.is_empty() {
146        let _ = writeln!(s, "inputs: {}", inputs.join(", "));
147    }
148    let mut last_layer: Option<Option<usize>> = None;
149    for (g, grp) in plan.groups.iter().enumerate() {
150        let first = &circuit.nodes[grp.nodes[0]];
151        if last_layer != Some(first.layer) {
152            let title = match first.layer {
153                Some(i) => format!("layer {i} ({})", circuit.layer_kinds[i].name()),
154                None => first.block.clone(),
155            };
156            let _ = writeln!(s, "== {title}");
157            last_layer = Some(first.layer);
158        }
159        let mut line = group_line(circuit, plan, g);
160        if let Some(n) = (notes.group)(grp) {
161            let _ = write!(line, " compute={n}");
162        }
163        let _ = writeln!(s, "{line}");
164        for &n in &grp.nodes {
165            if let Some(p) = (notes.node)(n) {
166                let _ = writeln!(s, "  {}: {p}", circuit.nodes[n].local);
167            }
168        }
169    }
170    s
171}
172
173fn group_line(circuit: &Circuit, plan: &FusionPlan, g: usize) -> String {
174    let grp = &plan.groups[g];
175    // 2026-09-29: A group with no kernels is work the host does outside the program: the
176    // prologue's embedding copy, or sampling from the logits. 2026-09-30: except a hardware
177    // plan's placeholder, which marks an op no kernel of the device's class covers.
178    let kernels = if grp.emitter == crate::hardware::plan::NOVEL_EMITTER {
179        "(novel: no kernel on this class)".to_string()
180    } else if !grp.runs.is_empty() {
181        // 2026-09-30: A per-run group: each run's launches, `!` marking a fragmented run.
182        grp.runs
183            .iter()
184            .map(|r| {
185                let l: Vec<String> = r
186                    .launches
187                    .iter()
188                    .map(|(k, t)| match t.count(r.run) {
189                        1 => k.to_string(),
190                        c => format!("{k} x{c}"),
191                    })
192                    .collect();
193                let copies = match r.copy_count() {
194                    0 => String::new(),
195                    c => format!(" + copy x{c}"),
196                };
197                format!(
198                    "[{}x{}{}: {}{copies}]",
199                    r.run.k,
200                    r.run.n,
201                    if r.run.contiguous { "" } else { "!" },
202                    l.join(" + ")
203                )
204            })
205            .collect::<Vec<_>>()
206            .join(" ")
207    } else if grp.kernels.is_empty() {
208        "(host)".to_string()
209    } else {
210        grp.kernels
211            .iter()
212            .map(|k| k.to_string())
213            .collect::<Vec<_>>()
214            .join(" + ")
215    };
216    let repeat = match grp.repeat.count(plan.rows) {
217        None | Some(1) => String::new(),
218        Some(n) => format!(" x{n}"),
219    };
220    let repeat = match grp.copies.and_then(|c| c.count(plan.rows)) {
221        Some(c) => format!("{repeat} + copy x{c}"),
222        None => repeat,
223    };
224    let numerics = match &grp.numerics {
225        Numerics::Reference => "reference".to_string(),
226        Numerics::BitIdentical { microtest } => format!("bit_identical({microtest})"),
227        Numerics::Differs { lever } => format!("differs({lever})"),
228    };
229    let block = circuit.nodes[grp.nodes[0]]
230        .id
231        .rsplit_once('.')
232        .map_or("", |(b, _)| b);
233    let members: Vec<&str> = grp
234        .nodes
235        .iter()
236        .map(|&n| circuit.nodes[n].local.as_str())
237        .collect();
238    let mut line = format!(
239        "g{g:04} {block} [{}] {kernels}{repeat} {numerics} rule={}",
240        members.join(","),
241        grp.rule
242    );
243    let mut fused = Vec::new();
244    let mut writes = Vec::new();
245    for &n in &grp.nodes {
246        for &e in &circuit.nodes[n].outputs {
247            let edge = &circuit.edges[e];
248            let local = edge
249                .id
250                .rsplit_once('.')
251                .map_or(edge.id.as_str(), |(_, l)| l);
252            match plan.edge_states[e] {
253                Some(EdgeState::Fused(_)) => fused.push(local.to_string()),
254                Some(EdgeState::Materialized) => {
255                    writes.push(format!("{local}:{}", plan.edge_formats[e]))
256                }
257                None => {}
258            }
259        }
260    }
261    if !fused.is_empty() {
262        let _ = write!(line, " fuses={{{}}}", fused.join(","));
263    }
264    if !writes.is_empty() {
265        let _ = write!(line, " writes={{{}}}", writes.join(","));
266    }
267    line
268}