1use std::io::{self, BufRead, Write};
7
8#[derive(Debug)]
10pub struct JitMethod {
11 pub name: String,
13 pub code_bytes: u32,
15 pub body: String,
17}
18
19fn empty_match_hint(pattern: &str, callers: &[&str]) -> String {
31 let p = pattern.trim().trim_matches('*');
32 if p.is_empty() {
33 return "no methods found\n".into();
34 }
35
36 if !callers.is_empty() {
43 let list: Vec<String> = callers
44 .iter()
45 .take(8)
46 .map(|c| format!(" • {c}"))
47 .collect();
48 let more = if callers.len() > 8 {
49 format!("\n … and {} more", callers.len() - 8)
50 } else {
51 String::new()
52 };
53 return format!(
54 "no standalone body for `{pattern}` — it was inlined at every call\n\
55 site. The inlined code runs, but lives embedded inside the callers'\n\
56 listings, not under its own header.\n\
57 \n\
58 Known callers (from the call graph of this capture):\n\
59 {}{more}\n\
60 \n\
61 Try `disasm {}` — the inlined body appears right after that\n\
62 call-site's argument setup. `search <tell-tale-instruction>` still\n\
63 works if you want to narrow down which caller actually hit a\n\
64 specific codegen variant.\n",
65 list.join("\n"),
66 callers[0],
67 );
68 }
69
70 format!(
74 "no methods found matching `{pattern}`.\n\
75 If the method is small/hot it was probably inlined — the JIT emits no\n\
76 standalone body for inlined methods, so it won't appear here. The\n\
77 inlined code still executes: it lives inside the caller's disasm.\n\
78 \n\
79 Workflow:\n\
80 1. Find a caller (the \"parent\" method that invokes it):\n\
81 `search <tell-tale-instruction-or-helper>`\n\
82 Pick an op the callee is likely to emit (a distinctive mask,\n\
83 shift, compare, or helper call). Every method whose body\n\
84 contains it is a caller that inlined the target.\n\
85 2. `disasm <parent-method>` — the inlined body appears embedded\n\
86 in the caller's listing, usually right after the call-site's\n\
87 argument setup.\n\
88 3. `methods *{p}*` — double-check the name isn't just qualified\n\
89 differently (generics, overloads, nested types).\n\
90 4. Only as a last resort: add `[MethodImpl(MethodImplOptions.NoInlining)]`\n\
91 for a one-off run to force standalone codegen. Revert after —\n\
92 it distorts everything else's disasm.\n"
93 )
94}
95
96fn is_zero_init_xor(line: &str) -> bool {
102 let lower = line.to_ascii_lowercase();
103 let (mnemonic, operands) = match lower.trim().split_once(|c: char| c.is_ascii_whitespace()) {
104 Some(pair) => pair,
105 None => return false,
106 };
107 if !matches!(mnemonic, "vxorps" | "vxorpd" | "vpxor" | "xorps" | "pxor") {
108 return false;
109 }
110 let ops: Vec<&str> = operands
111 .split(',')
112 .map(|t| t.split(';').next().unwrap_or("").trim())
113 .filter(|t| !t.is_empty())
114 .collect();
115 !ops.is_empty() && ops.iter().all(|r| *r == ops[0])
116}
117
118pub struct JitIndex {
120 pub methods: Vec<JitMethod>,
121 pub call_graph: std::collections::HashMap<String, Vec<String>>,
135}
136
137fn extract_call_target(line: &str) -> Option<(String, String)> {
144 let trimmed = line.trim_start();
145 let rest = trimmed.strip_prefix("call")?;
146 if !rest.starts_with(|c: char| c.is_ascii_whitespace()) {
149 return None;
150 }
151 let after = rest.trim_start();
152 let inside = after.strip_prefix('[').and_then(|s| s.split_once(']'))?.0;
153 let name = inside.split('(').next().unwrap_or(inside).trim();
155 if !name.contains(':') {
156 return None;
157 }
158 let short = match name.rsplit_once('.') {
159 Some((_, tail)) if tail.contains(':') => tail.to_string(),
160 _ => name.to_string(),
161 };
162 Some((name.to_string(), short))
163}
164
165impl JitIndex {
166 pub fn parse(text: &str) -> Self {
168 let mut methods = Vec::new();
169 let mut current_name: Option<String> = None;
170 let mut current_body = String::new();
171 let mut current_bytes: u32 = 0;
172
173 for line in text.lines() {
174 if let Some(rest) = line.strip_prefix("; Assembly listing for method ") {
175 if let Some(name) = current_name.take() {
177 methods.push(JitMethod {
178 name,
179 code_bytes: current_bytes,
180 body: std::mem::take(&mut current_body),
181 });
182 }
183 let name = rest
185 .rsplit_once(" (")
186 .map(|(n, _)| n)
187 .unwrap_or(rest)
188 .to_string();
189 current_name = Some(name);
190 current_body.clear();
191 current_bytes = 0;
192 }
193
194 if let Some(rest) = line.strip_prefix("; Total bytes of code") {
195 current_bytes = rest
197 .trim()
198 .trim_start_matches('=')
199 .trim()
200 .parse()
201 .unwrap_or(0);
202 }
203
204 if current_name.is_some() {
205 current_body.push_str(line);
206 current_body.push('\n');
207 }
208 }
209
210 if let Some(name) = current_name {
212 methods.push(JitMethod {
213 name,
214 code_bytes: current_bytes,
215 body: current_body,
216 });
217 }
218
219 let mut call_graph: std::collections::HashMap<String, Vec<String>> =
221 std::collections::HashMap::new();
222 for m in &methods {
223 let mut already: std::collections::HashSet<(String, String)> =
224 std::collections::HashSet::new();
225 for line in m.body.lines() {
226 if let Some((fq, short)) = extract_call_target(line) {
227 if already.insert((fq.clone(), short.clone())) {
230 call_graph.entry(fq).or_default().push(m.name.clone());
231 if !call_graph.get(&short).is_some_and(|v| v.contains(&m.name)) {
232 call_graph.entry(short).or_default().push(m.name.clone());
233 }
234 }
235 }
236 }
237 }
238
239 JitIndex {
240 methods,
241 call_graph,
242 }
243 }
244
245 pub fn callers_of(&self, pattern: &str) -> Vec<&str> {
252 let needle = pattern.trim().trim_matches('*');
253 if needle.is_empty() {
254 return Vec::new();
255 }
256 let mut seen = std::collections::HashSet::new();
257 let mut out: Vec<&str> = Vec::new();
258 for (callee, callers) in &self.call_graph {
259 if callee.contains(needle) {
260 for c in callers {
261 if seen.insert(c.as_str()) {
262 out.push(c.as_str());
263 }
264 }
265 }
266 }
267 out.sort_unstable();
268 out
269 }
270
271 fn filter(&self, pattern: &str) -> Vec<&JitMethod> {
273 if pattern.is_empty() || pattern == "." {
274 self.methods.iter().collect()
275 } else {
276 self.methods
277 .iter()
278 .filter(|m| m.name.contains(pattern))
279 .collect()
280 }
281 }
282
283 pub fn cmd_methods(&self, pattern: &str) -> String {
285 let mut matched = self.filter(pattern);
286 matched.sort_by(|a, b| b.code_bytes.cmp(&a.code_bytes));
287 let mut out = String::new();
288 for m in &matched {
289 out.push_str(&format!("{:<60} {} bytes\n", m.name, m.code_bytes));
290 }
291 if out.is_empty() {
292 let callers = self.callers_of(pattern);
293 out.push_str(&empty_match_hint(pattern, &callers));
294 }
295 out
296 }
297
298 pub fn disasm_with_parent_fallback(&self, pattern: &str) -> Option<String> {
312 let matched = self.filter(pattern);
313 if !matched.is_empty() {
314 let mut out = String::new();
315 for m in &matched {
316 out.push_str(&m.body);
317 out.push('\n');
318 }
319 return Some(out);
320 }
321
322 let callers = self.callers_of(pattern);
323 if callers.is_empty() {
324 return None;
325 }
326
327 const MAX: usize = 6;
332 let mut caller_methods: Vec<&JitMethod> = callers
333 .iter()
334 .filter_map(|c| self.methods.iter().find(|m| m.name == *c))
335 .collect();
336 caller_methods.sort_by(|a, b| b.code_bytes.cmp(&a.code_bytes));
337 let truncated = caller_methods.len() > MAX;
338 caller_methods.truncate(MAX);
339
340 let mut out = format!(
341 "── `{pattern}` has no standalone body — inlined at every call site. \
342 Showing {} caller listing(s); the inlined body is embedded in each. ──\n\n",
343 caller_methods.len()
344 );
345 for m in &caller_methods {
346 out.push_str(&format!("════════ parent: {} ════════\n", m.name));
347 out.push_str(&m.body);
348 out.push('\n');
349 }
350 if truncated {
351 out.push_str(&format!(
352 "\n(… {} more caller(s) omitted; request by name if needed.)\n",
353 callers.len() - MAX
354 ));
355 }
356 Some(out)
357 }
358
359 pub fn cmd_disasm(&self, pattern: &str) -> String {
363 self.disasm_with_parent_fallback(pattern)
364 .unwrap_or_else(|| empty_match_hint(pattern, &self.callers_of(pattern)))
365 }
366
367 pub fn cmd_search(&self, pattern: &str) -> String {
369 let mut out = String::new();
370 for m in &self.methods {
371 let hits: Vec<&str> = m
372 .body
373 .lines()
374 .filter(|l| !l.starts_with(';') && l.contains(pattern))
375 .collect();
376 if !hits.is_empty() {
377 out.push_str(&format!("{} ({} hits):\n", m.name, hits.len()));
378 for h in &hits {
379 out.push_str(&format!(" {}\n", h.trim()));
380 }
381 }
382 }
383 if out.is_empty() {
384 out.push_str("no matches\n");
385 }
386 out
387 }
388
389 fn extract_calls(body: &str) -> Vec<String> {
391 let mut targets = Vec::new();
392 for line in body.lines() {
393 let trimmed = line.trim();
394 if trimmed.starts_with(';') {
395 continue;
396 }
397 if let Some(rest) = trimmed.strip_prefix("call") {
399 let rest = rest.trim();
400 let target = rest
402 .strip_prefix('[')
403 .and_then(|s| s.strip_suffix(']'))
404 .unwrap_or(rest);
405 if !target.is_empty() {
406 targets.push(target.to_string());
407 }
408 }
409 }
410 targets
411 }
412
413 pub fn cmd_calls(&self, pattern: &str) -> String {
415 let matched = self.filter(pattern);
416 let mut out = String::new();
417 for m in &matched {
418 let targets = Self::extract_calls(&m.body);
419 if targets.is_empty() {
420 out.push_str(&format!("{}: no calls\n", m.name));
421 } else {
422 out.push_str(&format!("{} ({} calls):\n", m.name, targets.len()));
423 for t in &targets {
424 out.push_str(&format!(" → {}\n", t));
425 }
426 }
427 }
428 if out.is_empty() {
429 out.push_str("no methods found\n");
430 }
431 out
432 }
433
434 pub fn cmd_callers(&self, pattern: &str) -> String {
436 let mut out = String::new();
437 for m in &self.methods {
438 let targets = Self::extract_calls(&m.body);
439 let hits: Vec<&String> = targets.iter().filter(|t| t.contains(pattern)).collect();
440 if !hits.is_empty() {
441 out.push_str(&format!("{} calls it {} time(s):\n", m.name, hits.len()));
442 for t in &hits {
443 out.push_str(&format!(" → {}\n", t));
444 }
445 }
446 }
447 if out.is_empty() {
448 out.push_str(&format!("no callers found for '{}'\n", pattern));
449 }
450 out
451 }
452
453 pub fn cmd_stats(&self, pattern: &str) -> String {
455 let matched = self.filter(pattern);
456 if matched.is_empty() {
457 return "no methods found\n".into();
458 }
459
460 let total_bytes: u32 = matched.iter().map(|m| m.code_bytes).sum();
461
462 let instructions: Vec<&str> = matched
464 .iter()
465 .flat_map(|m| m.body.lines())
466 .filter(|l| !l.starts_with(';') && !l.is_empty())
467 .collect();
468
469 let count = |pats: &[&str]| -> usize {
470 instructions
471 .iter()
472 .filter(|l| pats.iter().any(|p| l.contains(p)))
473 .count()
474 };
475
476 let avx512 = count(&["zmm"]);
477 let avx2 = count(&["ymm"]);
478 let sse = count(&["xmm"]);
479 let fma = count(&["vfmadd", "vfmsub", "vfnmadd", "vfnmsub"]);
480 let neon = instructions
481 .iter()
482 .filter(|l| {
483 l.contains("{v")
484 && (l.contains("ld1")
485 || l.contains("st1")
486 || l.contains("fmla")
487 || l.contains("fmul"))
488 })
489 .count();
490 let sve = instructions
491 .iter()
492 .filter(|l| (l.contains("ld1w") || l.contains("st1w")) && l.contains("z"))
493 .count();
494 let bounds = count(&["RNGCHKFAIL"]);
495 let spills = instructions
496 .iter()
497 .filter(|l| l.contains("mov") && l.contains("[rsp"))
498 .count();
499
500 let label = if pattern.is_empty() || pattern == "." {
501 "--- all methods ---".to_string()
502 } else {
503 format!("--- filter: {} ---", pattern)
504 };
505
506 let mut out = format!("{}\n", label);
507 out.push_str(&format!("Methods: {}\n", matched.len()));
508 out.push_str(&format!("Total code: {} bytes\n", total_bytes));
509
510 if avx512 > 0 || avx2 > 0 || sse > 0 {
511 out.push_str(&format!("AVX-512 (zmm): {} instructions\n", avx512));
512 out.push_str(&format!("AVX2 (ymm): {} instructions\n", avx2));
513 out.push_str(&format!("SSE (xmm): {} instructions\n", sse));
514 }
515 if neon > 0 || sve > 0 {
516 out.push_str(&format!("NEON: {} instructions\n", neon));
517 out.push_str(&format!("SVE: {} instructions\n", sve));
518 }
519 if avx512 == 0 && avx2 == 0 && sse == 0 && neon == 0 && sve == 0 {
521 out.push_str("SIMD: none detected\n");
522 }
523 out.push_str(&format!("FMA: {} instructions\n", fma));
524 out.push_str(&format!("Bounds checks: {}\n", bounds));
525 out.push_str(&format!("Stack spills: {}\n", spills));
526 out
527 }
528
529 pub fn cmd_hotspots(&self, n: usize, pattern: &str) -> String {
531 let mut matched = self.filter(pattern);
532 matched.sort_by(|a, b| b.code_bytes.cmp(&a.code_bytes));
533 let mut out = String::new();
534 for m in matched.iter().take(n) {
535 out.push_str(&format!("{:<60} {} bytes\n", m.name, m.code_bytes));
536 }
537 if out.is_empty() {
538 out.push_str("no methods found\n");
539 }
540 out
541 }
542
543 pub fn cmd_simd_filtered(&self, pattern: &str) -> String {
546 const SIMD_PATTERNS: &[&str] = &[
547 "vmovups",
548 "vmovaps",
549 "vmulps",
550 "vaddps",
551 "vfmadd",
552 "vdpps",
553 "vxorps",
554 "vperm",
555 "vbroadcast",
556 "ld1",
558 "st1",
559 "fmla",
560 "fmul.v",
561 "fadd.v",
562 ];
563
564 let methods = self.filter(pattern);
565 let mut out = String::new();
566 for m in &methods {
567 let hits: Vec<&str> = m
568 .body
569 .lines()
570 .filter(|l| {
571 !l.starts_with(';')
572 && SIMD_PATTERNS.iter().any(|p| l.contains(p))
573 && !is_zero_init_xor(l)
574 })
575 .collect();
576 if !hits.is_empty() {
577 out.push_str(&format!("{} ({} hits):\n", m.name, hits.len()));
578 for h in &hits {
579 out.push_str(&format!(" {}\n", h.trim()));
580 }
581 }
582 }
583 if out.is_empty() {
584 out.push_str("no SIMD instructions found\n");
585 }
586 out
587 }
588}
589
590pub(crate) fn canonical_verb(cmd: &str) -> &str {
596 match cmd {
597 "jitdasm" => "disasm",
598 other => other,
599 }
600}
601
602pub fn run_repl(asm_path: &str, default_pattern: &str) -> io::Result<()> {
604 let text = std::fs::read_to_string(asm_path)?;
605 let index = JitIndex::parse(&text);
606
607 eprintln!("--- ready: {} methods captured ---", index.methods.len());
608 if !default_pattern.is_empty() {
609 eprintln!(
610 "--- default filter: `{}` (stats/simd/hotspots narrow to this) ---",
611 default_pattern
612 );
613 }
614 eprintln!("Type: help");
615
616 let stdin = io::stdin();
617 let mut stdout = io::stdout();
618
619 loop {
620 print!("jitdasm> ");
621 stdout.flush()?;
622
623 let mut line = String::new();
624 if stdin.lock().read_line(&mut line)? == 0 {
625 break; }
627 let line = line.trim();
628 if line.is_empty() {
629 continue;
630 }
631
632 let parts: Vec<&str> = line.splitn(3, ' ').collect();
633 let cmd = parts[0];
634 let arg1 = parts.get(1).copied().unwrap_or("");
635 let arg2 = parts.get(2).copied().unwrap_or("");
636
637 let pat = if arg2.is_empty() {
638 arg1.to_string()
639 } else {
640 format!("{arg1} {arg2}")
641 };
642
643 let stats_arg = if arg1.is_empty() {
646 default_pattern
647 } else {
648 arg1
649 };
650 let methods_arg = if arg1.is_empty() {
651 default_pattern
652 } else {
653 arg1
654 };
655 let hotspots_arg = if arg2.is_empty() {
656 default_pattern
657 } else {
658 arg2
659 };
660
661 let cmd = canonical_verb(cmd);
662
663 let result = match cmd {
664 "methods" => index.cmd_methods(methods_arg),
665 "disasm" if arg1.is_empty() && !default_pattern.is_empty() => {
666 index.cmd_disasm(default_pattern)
667 }
668 "disasm" if arg1.is_empty() => "usage: disasm <pattern>\n".into(),
669 "disasm" => index.cmd_disasm(&pat),
670 "search" if arg1.is_empty() => "usage: search <instruction>\n".into(),
671 "search" => index.cmd_search(arg1),
672 "stats" => index.cmd_stats(stats_arg),
673 "calls" if arg1.is_empty() => "usage: calls <pattern>\n".into(),
674 "calls" => index.cmd_calls(arg1),
675 "callers" if arg1.is_empty() => "usage: callers <pattern>\n".into(),
676 "callers" => index.cmd_callers(arg1),
677 "hotspots" => {
678 let n: usize = arg1.parse().unwrap_or(10);
679 index.cmd_hotspots(n, hotspots_arg)
680 }
681 "simd" => index.cmd_simd_filtered(default_pattern),
682 "help" => "jitdasm commands:\n \
683 methods [pattern] list methods with code sizes (sorted by size)\n \
684 disasm <pattern> show full disassembly for matching methods\n \
685 search <instruction> find methods containing an instruction\n \
686 stats [pattern] summary stats — scope to method, class, or namespace\n \
687 calls <pattern> what does this method call?\n \
688 callers <pattern> who calls this method?\n \
689 hotspots [N] [pat] top N methods by code size (default 10)\n \
690 simd find all methods using SIMD instructions\n \
691 help show this help\n \
692 exit quit\n"
693 .into(),
694 "exit" | "quit" => {
695 println!(
702 "\nREPL closed. The dbg session is still running in the background.\n\
703 Run `dbg kill` now to release the capture file and any subprocesses."
704 );
705 break;
706 }
707 _ => format!(
708 "unknown command: {}. Type 'help' for available commands.\n",
709 cmd
710 ),
711 };
712
713 print!("{}", result);
714 stdout.flush()?;
715 }
716
717 Ok(())
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 const SAMPLE: &str = include_str!("../tests/fixtures/jitdasm_sample.asm");
725
726 #[test]
732 fn empty_match_hint_plain_for_wildcard() {
733 assert_eq!(empty_match_hint("*", &[]), "no methods found\n");
737 assert_eq!(empty_match_hint("", &[]), "no methods found\n");
738 assert_eq!(empty_match_hint(" ", &[]), "no methods found\n");
739 }
740
741 #[test]
742 fn empty_match_hint_generic_when_no_callers_known() {
743 let msg = empty_match_hint("FlatLongIntMap:TryGetValue", &[]);
744 assert!(
745 msg.contains("inlined"),
746 "hint should mention inlining: {msg}"
747 );
748 assert!(
749 msg.contains("search"),
750 "hint should suggest `search`: {msg}"
751 );
752 assert!(
753 msg.contains("caller"),
754 "hint should tell the agent to look at the caller: {msg}"
755 );
756 assert!(
757 msg.contains("disasm <parent-method>"),
758 "hint should spell out the parent-disasm step: {msg}"
759 );
760 assert!(
761 msg.contains("NoInlining"),
762 "hint should mention NoInlining escape hatch: {msg}"
763 );
764 assert!(
765 msg.contains("FlatLongIntMap:TryGetValue"),
766 "hint should echo the pattern: {msg}"
767 );
768 }
769
770 #[test]
771 fn empty_match_hint_advertises_known_callers() {
772 let msg = empty_match_hint(
773 "MathUtils:Length",
774 &["MyNamespace.MathUtils:Normalize(float[]):float[]"],
775 );
776 assert!(
777 msg.contains("Known callers"),
778 "hint should label the caller list: {msg}"
779 );
780 assert!(
781 msg.contains("MathUtils:Normalize"),
782 "hint should name the actual caller: {msg}"
783 );
784 assert!(
785 msg.contains("disasm MyNamespace.MathUtils:Normalize"),
786 "hint should include a ready-to-run `disasm <caller>` command: {msg}"
787 );
788 assert!(
791 !msg.contains("NoInlining"),
792 "preemptive hint should skip the last-resort escape hatch: {msg}"
793 );
794 }
795
796 #[test]
797 fn extract_call_target_parses_managed_call() {
798 let (fq, short) = extract_call_target(
799 " call [MyNamespace.SimdOps:DotProduct(System.ReadOnlySpan`1[float],System.ReadOnlySpan`1[float]):float]",
800 )
801 .unwrap();
802 assert_eq!(fq, "MyNamespace.SimdOps:DotProduct");
803 assert_eq!(short, "SimdOps:DotProduct");
804 }
805
806 #[test]
807 fn extract_call_target_ignores_runtime_helpers() {
808 assert!(extract_call_target(" call CORINFO_HELP_RNGCHKFAIL").is_none());
809 assert!(extract_call_target(" call qword ptr [rax+0x10]").is_none());
810 assert!(extract_call_target(" mov rax, callable_ptr").is_none());
812 }
813
814 #[test]
815 fn call_graph_maps_inlined_callees_to_their_callers() {
816 let idx = JitIndex::parse(SAMPLE);
820 let callers = idx.callers_of("MathUtils:Length");
821 assert!(
822 callers.iter().any(|c| c.contains("MathUtils:Normalize")),
823 "MathUtils:Length should be advertised as called-by MathUtils:Normalize, got {callers:?}"
824 );
825 }
826
827 #[test]
828 fn cmd_disasm_of_inlined_method_emits_parent_body() {
829 let idx = JitIndex::parse(SAMPLE);
834 let out = idx.cmd_disasm("MathUtils:Length");
835
836 assert!(
838 out.contains("no standalone body"),
839 "output should flag that the target is inlined: {out}"
840 );
841 assert!(
842 out.contains("parent:") && out.contains("MathUtils:Normalize"),
843 "output should label the parent being shown: {out}"
844 );
845
846 assert!(
850 out.contains("call [MyNamespace.MathUtils:Length"),
851 "output should embed the parent's real disasm body, not a summary: {out}"
852 );
853 }
854
855 #[test]
856 fn cmd_disasm_falls_back_to_generic_hint_when_no_callers() {
857 let idx = JitIndex::parse(SAMPLE);
858 let out = idx.cmd_disasm("GhostMethod:Nope");
861 assert!(
862 out.contains("no methods found") || out.contains("NoInlining"),
863 "should fall back to the generic hint: {out}"
864 );
865 assert!(
866 !out.contains("parent:"),
867 "must not fabricate parent listings: {out}"
868 );
869 }
870
871 #[test]
872 fn canonical_verb_maps_jitdasm_to_disasm() {
873 assert_eq!(canonical_verb("jitdasm"), "disasm");
874 assert_eq!(canonical_verb("disasm"), "disasm");
875 assert_eq!(canonical_verb("methods"), "methods");
876 assert_eq!(canonical_verb("garbage"), "garbage");
877 }
878
879 #[test]
880 fn parse_finds_all_methods() {
881 let idx = JitIndex::parse(SAMPLE);
882 assert_eq!(idx.methods.len(), 4);
883 }
884
885 #[test]
886 fn parse_method_names() {
887 let idx = JitIndex::parse(SAMPLE);
888 let names: Vec<&str> = idx.methods.iter().map(|m| m.name.as_str()).collect();
889 assert!(
890 names
891 .iter()
892 .any(|n| n.contains("DotProduct") && !n.contains("Scalar"))
893 );
894 assert!(names.iter().any(|n| n.contains("ScalarDotProduct")));
895 assert!(names.iter().any(|n| n.contains("Normalize")));
896 assert!(names.iter().any(|n| n.contains("Pipeline:Run")));
897 }
898
899 #[test]
900 fn parse_code_bytes() {
901 let idx = JitIndex::parse(SAMPLE);
902 let dot = idx
903 .methods
904 .iter()
905 .find(|m| m.name.contains("DotProduct") && !m.name.contains("Scalar"))
906 .unwrap();
907 assert_eq!(dot.code_bytes, 250);
908 let scalar = idx
909 .methods
910 .iter()
911 .find(|m| m.name.contains("ScalarDotProduct"))
912 .unwrap();
913 assert_eq!(scalar.code_bytes, 96);
914 let norm = idx
915 .methods
916 .iter()
917 .find(|m| m.name.contains("Normalize"))
918 .unwrap();
919 assert_eq!(norm.code_bytes, 64);
920 let pipeline = idx
921 .methods
922 .iter()
923 .find(|m| m.name.contains("Pipeline"))
924 .unwrap();
925 assert_eq!(pipeline.code_bytes, 48);
926 }
927
928 #[test]
929 fn cmd_methods_lists_all() {
930 let idx = JitIndex::parse(SAMPLE);
931 let out = idx.cmd_methods("");
932 assert!(out.contains("DotProduct"));
933 assert!(out.contains("ScalarDotProduct"));
934 assert!(out.contains("Normalize"));
935 assert!(out.contains("Pipeline:Run"));
936 assert!(out.contains("250 bytes"));
937 assert!(out.contains("96 bytes"));
938 assert!(out.contains("64 bytes"));
939 assert!(out.contains("48 bytes"));
940 }
941
942 #[test]
943 fn cmd_simd_filtered_narrows_to_pattern() {
944 let idx = JitIndex::parse(SAMPLE);
947 let narrow = idx.cmd_simd_filtered("DotProduct");
948 let wide = idx.cmd_simd_filtered("");
949 assert!(wide.len() >= narrow.len(), "wide should be >= narrow");
951 assert!(
953 !narrow.contains("Normalize"),
954 "narrow leaked Normalize:\n{narrow}"
955 );
956 assert!(
957 !narrow.contains("Pipeline:Run"),
958 "narrow leaked Pipeline:\n{narrow}"
959 );
960 }
961
962 #[test]
963 fn cmd_stats_narrows_by_method_token() {
964 let idx = JitIndex::parse(SAMPLE);
967 let out = idx.cmd_stats(":DotProduct");
968 assert!(out.contains("filter:"), "expected filter label: {out}");
969 assert!(
970 out.contains("Methods: 1"),
971 "expected 1 method:\n{out}"
972 );
973 }
974
975 #[test]
976 fn cmd_methods_filtered_by_class() {
977 let idx = JitIndex::parse(SAMPLE);
978 let out = idx.cmd_methods("SimdOps");
979 assert!(out.contains("DotProduct"));
980 assert!(out.contains("ScalarDotProduct"));
981 assert!(!out.contains("Normalize"));
982 assert!(!out.contains("Pipeline"));
983 }
984
985 #[test]
986 fn cmd_stats_all() {
987 let idx = JitIndex::parse(SAMPLE);
988 let out = idx.cmd_stats("");
989 assert!(out.contains("Methods: 4"));
990 assert!(out.contains("Total code: 458 bytes"));
991 assert!(out.contains("Bounds checks: 2"));
992 }
993
994 #[test]
995 fn cmd_stats_filtered_by_class() {
996 let idx = JitIndex::parse(SAMPLE);
997 let out = idx.cmd_stats("SimdOps");
998 assert!(out.contains("Methods: 2"));
999 assert!(out.contains("Total code: 346 bytes"));
1000 }
1001
1002 #[test]
1003 fn cmd_stats_filtered_by_method() {
1004 let idx = JitIndex::parse(SAMPLE);
1005 let out = idx.cmd_stats("Normalize");
1006 assert!(out.contains("Methods: 1"));
1007 assert!(out.contains("Total code: 64 bytes"));
1008 }
1009
1010 #[test]
1011 fn cmd_stats_filtered_by_namespace() {
1012 let idx = JitIndex::parse(SAMPLE);
1013 let out = idx.cmd_stats("MyNamespace");
1014 assert!(out.contains("Methods: 4"));
1015 }
1016
1017 #[test]
1018 fn cmd_disasm_specific_method() {
1019 let idx = JitIndex::parse(SAMPLE);
1020 let out = idx.cmd_disasm("ScalarDotProduct");
1021 assert!(out.contains("ScalarDotProduct"));
1022 assert!(out.contains("vxorps xmm0"));
1023 assert!(!out.contains("vmovups")); }
1025
1026 #[test]
1027 fn cmd_search_instruction() {
1028 let idx = JitIndex::parse(SAMPLE);
1029 let out = idx.cmd_search("RNGCHKFAIL");
1030 assert!(out.contains("DotProduct"));
1031 assert!(out.contains("ScalarDotProduct"));
1032 assert!(!out.contains("Normalize"));
1033 }
1034
1035 #[test]
1036 fn cmd_search_spills() {
1037 let idx = JitIndex::parse(SAMPLE);
1038 let out = idx.cmd_search("[rsp");
1039 assert!(out.contains("Normalize"));
1040 }
1041
1042 #[test]
1043 fn cmd_hotspots_returns_sorted() {
1044 let idx = JitIndex::parse(SAMPLE);
1045 let out = idx.cmd_hotspots(10, "");
1046 let lines: Vec<&str> = out.lines().collect();
1047 assert!(lines[0].contains("250")); }
1049
1050 #[test]
1051 fn cmd_simd_finds_vectorized() {
1052 let idx = JitIndex::parse(SAMPLE);
1053 let out = idx.cmd_simd_filtered("");
1054 assert!(out.contains("DotProduct"));
1055 assert!(out.contains("vmovups"));
1056 assert!(out.contains("vmulps"));
1057 }
1058
1059 #[test]
1060 fn cmd_simd_ignores_vxorps_zero_init_idiom() {
1061 let asm = "\
1067; Assembly listing for method Broken.Program:SumSlow(System.Int32[]):int (Tier1)
1068G_M1_IG01:
1069 vxorps xmm0, xmm0, xmm0
1070 xor eax, eax
1071 mov edx, dword ptr [rcx+0x08]
1072 test edx, edx
1073 jle SHORT G_M1_IG03
1074G_M1_IG02:
1075 add eax, dword ptr [rcx+4*r8+0x10]
1076 inc r8d
1077 cmp r9d, r8d
1078 jl SHORT G_M1_IG02
1079G_M1_IG03:
1080 ret
1081
1082; Total bytes of code: 40
1083";
1084 let idx = JitIndex::parse(asm);
1085 let out = idx.cmd_simd_filtered("");
1086 assert!(
1087 out.contains("no SIMD instructions found"),
1088 "vxorps zero-init should not count as SIMD, got:\n{out}"
1089 );
1090 }
1094
1095 #[test]
1098 fn cmd_calls_shows_targets() {
1099 let idx = JitIndex::parse(SAMPLE);
1100 let out = idx.cmd_calls("Pipeline");
1101 assert!(out.contains("Pipeline:Run"));
1102 assert!(out.contains("→ MyNamespace.MathUtils:Normalize"));
1103 assert!(out.contains("→ MyNamespace.SimdOps:DotProduct"));
1104 assert!(out.contains("→ MyNamespace.SimdOps:ScalarDotProduct"));
1105 assert!(out.contains("3 calls"));
1106 }
1107
1108 #[test]
1109 fn cmd_calls_normalize() {
1110 let idx = JitIndex::parse(SAMPLE);
1111 let out = idx.cmd_calls("Normalize");
1112 assert!(out.contains("→ MyNamespace.SimdOps:DotProduct"));
1113 assert!(out.contains("→ MyNamespace.MathUtils:Length"));
1114 assert!(out.contains("2 calls"));
1115 }
1116
1117 #[test]
1118 fn cmd_calls_no_calls() {
1119 let idx = JitIndex::parse(SAMPLE);
1120 let out = idx.cmd_calls("ScalarDotProduct");
1122 assert!(out.contains("1 call")); }
1124
1125 #[test]
1126 fn cmd_callers_dotproduct() {
1127 let idx = JitIndex::parse(SAMPLE);
1128 let out = idx.cmd_callers("DotProduct");
1129 assert!(out.contains("Normalize"));
1131 assert!(out.contains("Pipeline:Run"));
1132 }
1133
1134 #[test]
1135 fn cmd_callers_normalize() {
1136 let idx = JitIndex::parse(SAMPLE);
1137 let out = idx.cmd_callers("Normalize");
1138 assert!(out.contains("Pipeline:Run"));
1140 assert!(!out.contains("DotProduct"));
1141 }
1142
1143 #[test]
1144 fn cmd_callers_nobody() {
1145 let idx = JitIndex::parse(SAMPLE);
1146 let out = idx.cmd_callers("Pipeline");
1147 assert!(out.contains("no callers found"));
1148 }
1149
1150 #[test]
1151 fn extract_calls_strips_brackets() {
1152 let body = " call [Foo:Bar(int):void]\n call CORINFO_HELP_RNGCHKFAIL\n";
1153 let calls = JitIndex::extract_calls(body);
1154 assert_eq!(calls.len(), 2);
1155 assert_eq!(calls[0], "Foo:Bar(int):void");
1156 assert_eq!(calls[1], "CORINFO_HELP_RNGCHKFAIL");
1157 }
1158
1159 #[test]
1160 fn extract_calls_skips_comments() {
1161 let body = "; call this is a comment\n call [Real:Call()]\n";
1162 let calls = JitIndex::extract_calls(body);
1163 assert_eq!(calls.len(), 1);
1164 assert_eq!(calls[0], "Real:Call()");
1165 }
1166
1167 #[test]
1168 fn parse_code_bytes_with_equals_format() {
1169 let asm = "; Assembly listing for method Foo:Bar()\n\
1171 ; Emitting BLENDED_CODE for X64\n\n\
1172 push rbp\n\
1173 ret\n\
1174 ; Total bytes of code = 42\n";
1175 let idx = JitIndex::parse(asm);
1176 assert_eq!(idx.methods.len(), 1);
1177 assert_eq!(idx.methods[0].code_bytes, 42);
1178 }
1179
1180 #[test]
1181 fn parse_code_bytes_plain_format() {
1182 let asm = "; Assembly listing for method Foo:Baz()\n\n\
1184 nop\n\
1185 ; Total bytes of code 250\n";
1186 let idx = JitIndex::parse(asm);
1187 assert_eq!(idx.methods.len(), 1);
1188 assert_eq!(idx.methods[0].code_bytes, 250);
1189 }
1190}