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