1use std::collections::BTreeMap;
13use std::fmt::Write as _;
14
15use crate::hir::{HirModule, HirNode, HirOp};
16use crate::lir::{LirBufferPlan, LirModule, LirViewAlias};
17use crate::mir::MirModule;
18use crate::phase::Phase;
19use crate::pretty::{header_line, op_kinds_line, pretty_print};
20use crate::{Graph, NodeId};
21
22pub fn inspect_hir(hir: &HirModule) -> String {
24 let mut out = String::new();
25 writeln!(
26 out,
27 "hir @{} ({} nodes, {} outputs, fusion={:?})",
28 hir.name,
29 hir.len(),
30 hir.outputs.len(),
31 hir.fusion_policy,
32 )
33 .unwrap();
34 writeln!(out, "{}", hir_op_kinds_line(hir)).unwrap();
35 writeln!(out).unwrap();
36
37 let mut tag_w = 0usize;
38 for node in hir.nodes() {
39 let t = hir_node_tag(node);
40 tag_w = tag_w.max(t.len());
41 }
42
43 for node in hir.nodes() {
44 let tag = hir_node_tag(node);
45 write!(out, " {tag:<width$} = ", width = tag_w).unwrap();
46 write!(out, "{}", format_hir_op(&node.op)).unwrap();
47 if !node.inputs.is_empty() {
48 write!(out, "(").unwrap();
49 for (i, inp) in node.inputs.iter().enumerate() {
50 if i > 0 {
51 write!(out, ", ").unwrap();
52 }
53 write!(out, "{inp}").unwrap();
54 }
55 write!(out, ")").unwrap();
56 }
57 write!(out, " : {}", node.shape).unwrap();
58 if hir.outputs.contains(&node.id) {
59 write!(out, " ← output").unwrap();
60 }
61 writeln!(out).unwrap();
62 }
63 if !hir.outputs.is_empty() {
64 write!(out, " return ").unwrap();
65 for (i, o) in hir.outputs.iter().enumerate() {
66 if i > 0 {
67 write!(out, ", ").unwrap();
68 }
69 write!(out, "{o}").unwrap();
70 }
71 writeln!(out).unwrap();
72 }
73 out
74}
75
76pub fn inspect_mir(mir: &MirModule) -> String {
78 inspect_mir_with_diff(mir, None)
79}
80
81pub fn inspect_mir_with_diff(mir: &MirModule, before: Option<&MirModule>) -> String {
83 let g = mir.as_graph();
84 let mut out = String::new();
85 writeln!(out, "mir @{} {{", mir.name()).unwrap();
86 if let Some(b) = before {
87 writeln!(out).unwrap();
88 out.push_str(&inspect_graph_diff(b.as_graph(), g));
89 writeln!(out).unwrap();
90 writeln!(out, "--- graph ---").unwrap();
91 }
92 writeln!(out).unwrap();
93 out.push_str(&pretty_print(g));
94 if !out.ends_with('\n') {
95 out.push('\n');
96 }
97 write!(out, "}}").unwrap();
98 out
99}
100
101pub fn inspect_mir_diff(before: &MirModule, after: &MirModule) -> String {
103 inspect_graph_diff(before.as_graph(), after.as_graph())
104}
105
106pub fn inspect_graph_diff(before: &Graph, after: &Graph) -> String {
108 use std::collections::BTreeMap;
109
110 let mut out = String::new();
111 writeln!(
112 out,
113 " diff: {} → {} nodes ({} → {} outputs)",
114 before.len(),
115 after.len(),
116 before.outputs.len(),
117 after.outputs.len(),
118 )
119 .unwrap();
120
121 let count_kinds = |g: &Graph| {
122 let mut h: BTreeMap<String, i32> = BTreeMap::new();
123 for n in g.nodes() {
124 *h.entry(format!("{:?}", n.op.kind())).or_insert(0) += 1;
125 }
126 h
127 };
128 let b = count_kinds(before);
129 let a = count_kinds(after);
130 let mut keys: Vec<String> = b.keys().chain(a.keys()).cloned().collect();
131 keys.sort();
132 keys.dedup();
133 let mut changes = Vec::new();
134 for k in keys {
135 let d = a.get(&k).copied().unwrap_or(0) - b.get(&k).copied().unwrap_or(0);
136 if d != 0 {
137 changes.push(format!("{k}{d:+}"));
138 }
139 }
140 if !changes.is_empty() {
141 writeln!(out, " op delta: {}", changes.join(", ")).unwrap();
142 }
143 out
144}
145
146pub fn inspect_lir(lir: &LirModule) -> String {
148 let mut out = String::new();
149 writeln!(out, "lir @{} {{", lir.name()).unwrap();
150 writeln!(out, " fingerprint: {:016x}", lir.fingerprint().0).unwrap();
151 writeln!(out).unwrap();
152 out.push_str(&inspect_buffer_plan(&lir.buffers));
153 if !lir.buffers.phases.is_empty() {
154 writeln!(out).unwrap();
155 out.push_str(&inspect_phases(&lir.buffers));
156 }
157 if !lir.buffers.io.inputs.is_empty() || !lir.buffers.io.params.is_empty() {
158 writeln!(out).unwrap();
159 out.push_str(&inspect_io_manifest(&lir.buffers));
160 }
161 writeln!(out).unwrap();
162 writeln!(out, "--- mir ---").unwrap();
163 out.push_str(&pretty_print(lir.as_graph()));
164 if !out.ends_with('\n') {
165 out.push('\n');
166 }
167 write!(out, "}}").unwrap();
168 out
169}
170
171pub fn inspect_graph(g: &Graph) -> String {
173 pretty_print(g)
174}
175
176pub fn inspect_hir_stats(hir: &HirModule) -> String {
178 format!(
179 "hir @{} ({} nodes, {} outputs, fusion={:?})\n{}",
180 hir.name,
181 hir.len(),
182 hir.outputs.len(),
183 hir.fusion_policy,
184 hir_op_kinds_line(hir),
185 )
186}
187
188pub fn inspect_mir_stats(mir: &MirModule) -> String {
190 let g = mir.as_graph();
191 format!(
192 "mir @{} — {}\n{}",
193 mir.name(),
194 header_line(g),
195 op_kinds_line(g),
196 )
197}
198
199pub fn inspect_buffer_plan(plan: &LirBufferPlan) -> String {
201 let mut out = String::new();
202 let saved = plan.bytes_saved();
203 let naive = plan.total_unshared_bytes();
204 writeln!(
205 out,
206 " arena: {} bytes (saved {} vs {} naive, align={})",
207 plan.arena_size, saved, naive, plan.alignment,
208 )
209 .unwrap();
210 writeln!(
211 out,
212 " schedule: {} nodes, {} views",
213 plan.schedule.len(),
214 plan.view_aliases.len(),
215 )
216 .unwrap();
217 if !plan.dynamic_symbols.is_empty() {
218 let syms: Vec<String> = plan
219 .dynamic_symbols
220 .iter()
221 .map(|s| format!("?{s}"))
222 .collect();
223 writeln!(out, " dynamic: {}", syms.join(", ")).unwrap();
224 }
225 writeln!(out).unwrap();
226 writeln!(out, " # offset\tsize\tnode").unwrap();
227
228 let mut rows: Vec<(usize, usize, NodeId)> = plan
229 .assignments
230 .iter()
231 .map(|(id, slot)| (slot.offset, slot.size, *id))
232 .collect();
233 rows.sort_by_key(|(off, _, _)| *off);
234 for (off, sz, id) in rows {
235 let sched = plan
236 .schedule
237 .iter()
238 .position(|&n| n == id)
239 .map(|i| format!(" sched={i}"))
240 .unwrap_or_default();
241 let view = plan
242 .view_aliases
243 .get(&id)
244 .map(|LirViewAlias { root, byte_offset }| format!(" view→{root}+{byte_offset}"))
245 .unwrap_or_default();
246 let phase = plan
247 .phases
248 .get(id)
249 .map(|p| format!(" {p:?}"))
250 .unwrap_or_default();
251 writeln!(out, " {off}\t{sz}\t{id}{sched}{view}{phase}").unwrap();
252 }
253 out
254}
255
256fn inspect_phases(plan: &LirBufferPlan) -> String {
257 let mut out = String::from(" phases:\n");
258 for phase in [Phase::Prologue, Phase::SteadyState, Phase::Epilogue] {
259 let nodes = plan.nodes_in_phase(phase);
260 if !nodes.is_empty() {
261 writeln!(out, " {phase:?}: {nodes:?}").unwrap();
262 }
263 }
264 out
265}
266
267fn inspect_io_manifest(plan: &LirBufferPlan) -> String {
268 let mut out = String::from(" io:\n");
269 for (name, id) in &plan.io.inputs {
270 writeln!(out, " input \"{name}\" → {id}").unwrap();
271 }
272 for (name, id) in &plan.io.params {
273 writeln!(out, " param \"{name}\" → {id}").unwrap();
274 }
275 if !plan.io.outputs.is_empty() {
276 write!(out, " outputs: {:?}", plan.io.outputs).unwrap();
277 out.push('\n');
278 }
279 out
280}
281
282fn hir_op_kinds_line(hir: &HirModule) -> String {
283 let mut hist: BTreeMap<String, usize> = BTreeMap::new();
284 for node in hir.nodes() {
285 *hist.entry(hir_op_kind(&node.op)).or_insert(0) += 1;
286 }
287 let parts: Vec<String> = hist.into_iter().map(|(k, c)| format!("{k}={c}")).collect();
288 format!(" block ops: {}", parts.join(", "))
289}
290
291fn hir_op_kind(op: &HirOp) -> String {
292 match op {
293 HirOp::Input { .. } => "Input".into(),
294 HirOp::Param { .. } => "Param".into(),
295 HirOp::Constant { .. } => "Constant".into(),
296 HirOp::Linear { .. } => "Linear".into(),
297 HirOp::LinearFused { .. } => "LinearFused".into(),
298 HirOp::SharedLinearPair { .. } => "SharedLinearPair".into(),
299 HirOp::SwiGLU => "SwiGLU".into(),
300 HirOp::ResidualRmsNorm { .. } => "ResidualRmsNorm".into(),
301 HirOp::Attention { .. } => "Attention".into(),
302 HirOp::DepthwiseConv1dCausal { .. } => "DepthwiseConv1dCausal".into(),
303 HirOp::DequantMatMul { .. } => "DequantMatMul".into(),
304 HirOp::GatedDeltaNet { .. } => "GatedDeltaNet".into(),
305 HirOp::Lstm { .. } => "Lstm".into(),
306 HirOp::Gru { .. } => "Gru".into(),
307 HirOp::RoPE { .. } => "RoPE".into(),
308 HirOp::RmsNorm { .. } => "RmsNorm".into(),
309 HirOp::Mir(_) => "Mir".into(),
310 HirOp::LlamaDecoderBlock { .. } => "LlamaDecoderBlock".into(),
311 HirOp::Qwen35MtpHead { .. } => "Qwen35MtpHead".into(),
312 }
313}
314
315fn hir_node_tag(node: &HirNode) -> String {
316 let label: Option<String> = match &node.op {
317 HirOp::Input { name } => Some(format!("input \"{name}\"")),
318 HirOp::Param { name } => Some(format!("param \"{name}\"")),
319 _ => node.name.as_deref().map(|s| format!("\"{s}\"")),
320 };
321 match label {
322 Some(s) => format!("{} [{s}]", node.id),
323 None => format!("{}", node.id),
324 }
325}
326
327fn format_hir_op(op: &HirOp) -> String {
328 match op {
329 HirOp::Input { name } => format!("input(\"{name}\")"),
330 HirOp::Param { name } => format!("param(\"{name}\")"),
331 HirOp::Constant { data } => format!("constant({} bytes)", data.len()),
332 HirOp::Linear {
333 activation,
334 has_bias,
335 } => {
336 let mut s = String::from("linear");
337 if *has_bias {
338 s.push_str("+bias");
339 }
340 if let Some(act) = activation {
341 write!(s, "+{act:?}").unwrap();
342 }
343 s
344 }
345 HirOp::LinearFused { activation } => match activation {
346 Some(act) => format!("linear_fused({act:?})"),
347 None => "linear_fused".into(),
348 },
349 HirOp::SharedLinearPair { slot } => format!("shared_linear_pair(out={slot})"),
350 HirOp::SwiGLU => "swiglu_ffn".into(),
351 HirOp::ResidualRmsNorm { eps } => format!("residual_rms_norm(eps={eps})"),
352 HirOp::Attention {
353 num_heads,
354 head_dim,
355 mask,
356 } => format!("attention(heads={num_heads}, dim={head_dim}, mask={mask:?})"),
357 HirOp::DepthwiseConv1dCausal { kernel_size } => {
358 format!("depthwise_conv1d_causal(k={kernel_size})")
359 }
360 HirOp::DequantMatMul { scheme } => format!("dequant_matmul({scheme})"),
361 HirOp::GatedDeltaNet {
362 state_size,
363 carry_state,
364 gate_per_channel,
365 } => {
366 let gpc = if *gate_per_channel { ",gpc" } else { "" };
367 if *carry_state {
368 format!("gated_delta_net(n={state_size},carry{gpc})")
369 } else {
370 format!("gated_delta_net(n={state_size}{gpc})")
371 }
372 }
373 HirOp::Lstm {
374 hidden_size,
375 num_layers,
376 bidirectional,
377 ..
378 } => {
379 let dir = if *bidirectional { "bi" } else { "uni" };
380 format!("lstm(h={hidden_size},layers={num_layers},{dir})")
381 }
382 HirOp::Gru {
383 hidden_size,
384 num_layers,
385 bidirectional,
386 ..
387 } => {
388 let dir = if *bidirectional { "bi" } else { "uni" };
389 format!("gru(h={hidden_size},layers={num_layers},{dir})")
390 }
391 HirOp::RoPE { head_dim, n_rot } => format!("rope(d={head_dim}, n_rot={n_rot})"),
392 HirOp::RmsNorm { eps } => format!("rms_norm(eps={eps})"),
393 HirOp::LlamaDecoderBlock {
394 num_heads,
395 head_dim,
396 num_kv_heads,
397 eps,
398 mask,
399 rope_style,
400 } => format!(
401 "llama_decoder_block(heads={num_heads}, dim={head_dim}, kv={num_kv_heads}, eps={eps}, mask={mask:?}, rope={rope_style:?})"
402 ),
403 HirOp::Qwen35MtpHead {
404 num_heads,
405 head_dim,
406 mtp_vocab,
407 ..
408 } => format!("qwen35_mtp_head(heads={num_heads}, dim={head_dim}, vocab={mtp_vocab})"),
409 HirOp::Mir(inner) => format!("mir({inner})"),
410 }
411}
412
413impl HirModule {
416 pub fn inspect(&self) -> String {
418 inspect_hir(self)
419 }
420}
421
422impl MirModule {
423 pub fn inspect(&self) -> String {
425 inspect_mir(self)
426 }
427}
428
429impl LirModule {
430 pub fn inspect(&self) -> String {
432 inspect_lir(self)
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::DType;
440 use crate::Shape;
441
442 fn f32_shape(d: &[usize]) -> Shape {
443 Shape::new(d, DType::F32)
444 }
445
446 #[test]
447 fn inspect_hir_includes_blocks_and_outputs() {
448 let mut hir = HirModule::new("layer");
449 let x = hir.input("x", f32_shape(&[2, 128]));
450 let w = hir.param("w", f32_shape(&[128, 128]));
451 let h = hir.linear(x, w, None, None, f32_shape(&[2, 128]));
452 hir.outputs = vec![h];
453
454 let text = inspect_hir(&hir);
455 assert!(text.contains("hir @layer"));
456 assert!(text.contains("linear"));
457 assert!(text.contains("← output"));
458 assert!(text.contains("fusion=Direct"));
459 }
460
461 #[test]
462 fn inspect_mir_wraps_pretty_print() {
463 let mut hir = HirModule::new("m");
464 let x = hir.input("x", f32_shape(&[4]));
465 hir.outputs = vec![x];
466 let mir = hir.lower_to_mir().expect("lower");
467
468 let text = inspect_mir(&mir);
469 assert!(text.contains("mir @m"));
470 assert!(text.contains("graph @m"));
471 assert!(text.contains("input(\"x\")"));
472 }
473
474 #[test]
475 fn named_block_appears_in_hir_dump() {
476 let mut hir = HirModule::new("layer");
477 let x = hir.input("x", f32_shape(&[2, 8]));
478 let w = hir.param("w", f32_shape(&[8, 8]));
479 let out = hir.named("layer0.ffn", |h| {
480 h.linear(x, w, None, None, f32_shape(&[2, 8]))
481 });
482 hir.outputs = vec![out];
483
484 let text = inspect_hir(&hir);
485 assert!(text.contains("layer0.ffn"));
486 }
487
488 #[test]
489 fn provenance_survives_lower() {
490 let mut hir = HirModule::new("m");
491 let x = hir.input("x", f32_shape(&[2, 8]));
492 let w = hir.param("w", f32_shape(&[8, 8]));
493 let out = hir.named("block", |h| h.linear(x, w, None, None, f32_shape(&[2, 8])));
494 hir.outputs = vec![out];
495
496 let mir = hir.lower_to_mir().expect("lower");
497 let text = inspect_mir(&mir);
498 assert!(text.contains("hir=h"));
499 assert!(text.contains("block"));
500 }
501
502 #[test]
503 fn inspect_lir_includes_buffer_plan() {
504 use crate::lir::{LirBufferPlan, LirBufferSlot, LirIoManifest};
505
506 let mut hir = HirModule::new("l");
507 let x = hir.input("x", f32_shape(&[4]));
508 hir.outputs = vec![x];
509 let mir = hir.lower_to_mir().expect("lower");
510 let plan = LirBufferPlan {
511 arena_size: 16,
512 assignments: [(
513 NodeId(0),
514 LirBufferSlot {
515 offset: 0,
516 size: 16,
517 },
518 )]
519 .into_iter()
520 .collect(),
521 schedule: vec![NodeId(0)],
522 io: LirIoManifest {
523 inputs: vec![("x".into(), NodeId(0))],
524 ..Default::default()
525 },
526 ..Default::default()
527 };
528 let lir = LirModule::new(mir, plan);
529
530 let text = inspect_lir(&lir);
531 assert!(text.contains("lir @l"));
532 assert!(text.contains("arena: 16 bytes"));
533 assert!(text.contains("fingerprint:"));
534 assert!(text.contains("--- mir ---"));
535 }
536}