plotnik_vm/engine/
trace.rs

1//! Tracing infrastructure for debugging VM execution.
2//!
3//! # Design: Zero-Cost Abstraction
4//!
5//! The tracer is designed as a zero-cost abstraction. When `NoopTracer` is used:
6//! - All trait methods are `#[inline(always)]` empty functions
7//! - The compiler eliminates all tracer calls and their arguments
8//! - No tracing-related state exists in core execution structures
9//!
10//! # Design: Tracer-Owned State
11//!
12//! Tracing-only state (like checkpoint creation IPs for backtrack display) is
13//! maintained by the tracer itself, not in core structures like `Checkpoint`.
14//! This keeps execution structures minimal and avoids "spilling" tracing concerns
15//! into `exec`. For example:
16//! - `trace_checkpoint_created(ip)` - tracer pushes to its own stack
17//! - `trace_backtrack()` - tracer pops its stack to get the display IP
18//!
19//! `NoopTracer` ignores these calls (optimized away), while `PrintTracer`
20//! maintains parallel state for display purposes.
21
22use std::num::NonZeroU16;
23
24use arborium_tree_sitter::Node;
25
26use plotnik_bytecode::{
27    EffectOpcode, Instruction, LineBuilder, Match, Module, Nav, NodeTypeIR, PredicateOp, Symbol,
28    cols, format_effect, trace, truncate_text, width_for_count,
29};
30use plotnik_core::Colors;
31
32use super::effect::RuntimeEffect;
33
34/// Verbosity level for trace output.
35///
36/// Controls which sub-lines are shown and whether node text is included.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum Verbosity {
39    /// Default: match, backtrack, call/return. Kind only, no text.
40    #[default]
41    Default,
42    /// Verbose (-v): all sub-lines. Text on match/failure.
43    Verbose,
44    /// Very verbose (-vv): all sub-lines. Text on everything including nav.
45    VeryVerbose,
46}
47
48/// Tracer trait for VM execution instrumentation.
49///
50/// All methods receive raw data (IDs, nodes) that the VM already has.
51/// Formatting and name resolution happen in the tracer implementation.
52///
53/// Each method is called at a specific point during execution:
54/// - `trace_instruction` - before executing an instruction
55/// - `trace_nav` - after navigation succeeds
56/// - `trace_match_success/failure` - after type check
57/// - `trace_field_success/failure` - after field check
58/// - `trace_effect` - after emitting an effect
59/// - `trace_call` - when entering a definition
60/// - `trace_return` - when returning from a definition
61/// - `trace_checkpoint_created` - when a checkpoint is pushed
62/// - `trace_backtrack` - when restoring a checkpoint
63/// - `trace_enter_entrypoint` - when entering an entrypoint (for labels)
64pub trait Tracer {
65    /// Called before executing an instruction.
66    fn trace_instruction(&mut self, ip: u16, instr: &Instruction<'_>);
67
68    /// Called after navigation succeeds.
69    fn trace_nav(&mut self, nav: Nav, node: Node<'_>);
70
71    /// Called when navigation fails (no child/sibling exists).
72    fn trace_nav_failure(&mut self, nav: Nav);
73
74    /// Called after type check succeeds.
75    fn trace_match_success(&mut self, node: Node<'_>);
76
77    /// Called after type check fails.
78    fn trace_match_failure(&mut self, node: Node<'_>);
79
80    /// Called after field check succeeds.
81    fn trace_field_success(&mut self, field_id: NonZeroU16);
82
83    /// Called after field check fails.
84    fn trace_field_failure(&mut self, node: Node<'_>);
85
86    /// Called after emitting an effect.
87    fn trace_effect(&mut self, effect: &RuntimeEffect<'_>);
88
89    /// Called when an effect is suppressed (inside @_ capture).
90    fn trace_effect_suppressed(&mut self, opcode: EffectOpcode, payload: usize);
91
92    /// Called for SuppressBegin/SuppressEnd control effects.
93    /// `suppressed` is true if already inside another suppress scope.
94    fn trace_suppress_control(&mut self, opcode: EffectOpcode, suppressed: bool);
95
96    /// Called when entering a definition via Call.
97    fn trace_call(&mut self, target_ip: u16);
98
99    /// Called when returning from a definition.
100    fn trace_return(&mut self);
101
102    /// Called when a checkpoint is created.
103    fn trace_checkpoint_created(&mut self, ip: u16);
104
105    /// Called when backtracking occurs.
106    fn trace_backtrack(&mut self);
107
108    /// Called when entering an entrypoint (for section labels).
109    fn trace_enter_entrypoint(&mut self, target_ip: u16);
110
111    /// Called when entering the preamble (bootstrap wrapper).
112    fn trace_enter_preamble(&mut self);
113}
114
115/// No-op tracer that gets optimized away completely.
116pub struct NoopTracer;
117
118impl Tracer for NoopTracer {
119    #[inline(always)]
120    fn trace_instruction(&mut self, _ip: u16, _instr: &Instruction<'_>) {}
121
122    #[inline(always)]
123    fn trace_nav(&mut self, _nav: Nav, _node: Node<'_>) {}
124
125    #[inline(always)]
126    fn trace_nav_failure(&mut self, _nav: Nav) {}
127
128    #[inline(always)]
129    fn trace_match_success(&mut self, _node: Node<'_>) {}
130
131    #[inline(always)]
132    fn trace_match_failure(&mut self, _node: Node<'_>) {}
133
134    #[inline(always)]
135    fn trace_field_success(&mut self, _field_id: NonZeroU16) {}
136
137    #[inline(always)]
138    fn trace_field_failure(&mut self, _node: Node<'_>) {}
139
140    #[inline(always)]
141    fn trace_effect(&mut self, _effect: &RuntimeEffect<'_>) {}
142
143    #[inline(always)]
144    fn trace_effect_suppressed(&mut self, _opcode: EffectOpcode, _payload: usize) {}
145
146    #[inline(always)]
147    fn trace_suppress_control(&mut self, _opcode: EffectOpcode, _suppressed: bool) {}
148
149    #[inline(always)]
150    fn trace_call(&mut self, _target_ip: u16) {}
151
152    #[inline(always)]
153    fn trace_return(&mut self) {}
154
155    #[inline(always)]
156    fn trace_checkpoint_created(&mut self, _ip: u16) {}
157
158    #[inline(always)]
159    fn trace_backtrack(&mut self) {}
160
161    #[inline(always)]
162    fn trace_enter_entrypoint(&mut self, _target_ip: u16) {}
163
164    #[inline(always)]
165    fn trace_enter_preamble(&mut self) {}
166}
167
168use std::collections::BTreeMap;
169
170/// Tracer that collects execution trace for debugging.
171pub struct PrintTracer<'s> {
172    /// Source code for extracting node text.
173    pub(crate) source: &'s [u8],
174    /// Verbosity level for output filtering.
175    pub(crate) verbosity: Verbosity,
176    /// Collected trace lines.
177    pub(crate) lines: Vec<String>,
178    /// Line builder for formatting.
179    pub(crate) builder: LineBuilder,
180    /// Maps node type ID to name.
181    pub(crate) node_type_names: BTreeMap<u16, String>,
182    /// Maps node field ID to name.
183    pub(crate) node_field_names: BTreeMap<u16, String>,
184    /// Maps member index to name (for Set/Enum effect display).
185    pub(crate) member_names: Vec<String>,
186    /// All strings from StringTable (for predicate value display).
187    pub(crate) all_strings: Vec<String>,
188    /// Regex patterns indexed by RegexId (for predicate display).
189    pub(crate) regex_patterns: Vec<String>,
190    /// Maps entrypoint target IP to name (for labels and call/return).
191    pub(crate) entrypoint_by_ip: BTreeMap<u16, String>,
192    /// Parallel stack of checkpoint creation IPs (for backtrack display).
193    pub(crate) checkpoint_ips: Vec<u16>,
194    /// Stack of definition names (for return display).
195    pub(crate) definition_stack: Vec<String>,
196    /// Pending return instruction IP (for consolidated return line).
197    pub(crate) pending_return_ip: Option<u16>,
198    /// Step width for formatting.
199    pub(crate) step_width: usize,
200    /// Color palette.
201    pub(crate) colors: Colors,
202    /// Previous instruction IP (for cache line boundary detection).
203    pub(crate) prev_ip: Option<u16>,
204}
205
206/// Builder for `PrintTracer`.
207pub struct PrintTracerBuilder<'s, 'm> {
208    source: &'s str,
209    module: &'m Module,
210    verbosity: Verbosity,
211    colors: Colors,
212}
213
214impl<'s, 'm> PrintTracerBuilder<'s, 'm> {
215    /// Create a new builder with required parameters.
216    pub fn new(source: &'s str, module: &'m Module) -> Self {
217        Self {
218            source,
219            module,
220            verbosity: Verbosity::Default,
221            colors: Colors::OFF,
222        }
223    }
224
225    /// Set the verbosity level.
226    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
227        self.verbosity = verbosity;
228        self
229    }
230
231    /// Set whether to use colored output.
232    pub fn colored(mut self, enabled: bool) -> Self {
233        self.colors = Colors::new(enabled);
234        self
235    }
236
237    /// Build the PrintTracer.
238    pub fn build(self) -> PrintTracer<'s> {
239        let header = self.module.header();
240        let strings = self.module.strings();
241        let regexes = self.module.regexes();
242        let types = self.module.types();
243        let node_types = self.module.node_types();
244        let node_fields = self.module.node_fields();
245        let entrypoints = self.module.entrypoints();
246
247        let mut node_type_names = BTreeMap::new();
248        for i in 0..node_types.len() {
249            let t = node_types.get(i);
250            node_type_names.insert(t.id, strings.get(t.name).to_string());
251        }
252
253        let mut node_field_names = BTreeMap::new();
254        for i in 0..node_fields.len() {
255            let f = node_fields.get(i);
256            node_field_names.insert(f.id, strings.get(f.name).to_string());
257        }
258
259        // Build member names lookup (index -> name)
260        let member_names: Vec<String> = (0..types.members_count())
261            .map(|i| strings.get(types.get_member(i).name).to_string())
262            .collect();
263
264        // Build all_strings for predicate value display (same as dump.rs)
265        let all_strings: Vec<String> = (0..header.str_table_count as usize)
266            .map(|i| strings.get_by_index(i).to_string())
267            .collect();
268
269        // Build regex patterns (indexed by RegexId → pattern string)
270        // Index 0 is reserved, so we start with an empty string placeholder
271        let mut regex_patterns = vec![String::new()];
272        for i in 1..header.regex_table_count as usize {
273            let string_id = regexes.get_string_id(i);
274            regex_patterns.push(strings.get(string_id).to_string());
275        }
276
277        // Build entrypoint IP -> name lookup
278        let mut entrypoint_by_ip = BTreeMap::new();
279        for i in 0..entrypoints.len() {
280            let e = entrypoints.get(i);
281            entrypoint_by_ip.insert(e.target, strings.get(e.name).to_string());
282        }
283
284        let step_width = width_for_count(header.transitions_count as usize);
285
286        PrintTracer {
287            source: self.source.as_bytes(),
288            verbosity: self.verbosity,
289            lines: Vec::new(),
290            builder: LineBuilder::new(step_width),
291            node_type_names,
292            node_field_names,
293            member_names,
294            all_strings,
295            regex_patterns,
296            entrypoint_by_ip,
297            checkpoint_ips: Vec::new(),
298            definition_stack: Vec::new(),
299            pending_return_ip: None,
300            step_width,
301            colors: self.colors,
302            prev_ip: None,
303        }
304    }
305}
306
307impl<'s> PrintTracer<'s> {
308    /// Create a builder for PrintTracer.
309    pub fn builder<'m>(source: &'s str, module: &'m Module) -> PrintTracerBuilder<'s, 'm> {
310        PrintTracerBuilder::new(source, module)
311    }
312
313    fn node_type_name(&self, id: u16) -> &str {
314        self.node_type_names.get(&id).map_or("?", |s| s.as_str())
315    }
316
317    fn node_field_name(&self, id: u16) -> &str {
318        self.node_field_names.get(&id).map_or("?", |s| s.as_str())
319    }
320
321    fn member_name(&self, idx: u16) -> &str {
322        self.member_names
323            .get(idx as usize)
324            .map_or("?", |s| s.as_str())
325    }
326
327    fn entrypoint_name(&self, ip: u16) -> &str {
328        self.entrypoint_by_ip.get(&ip).map_or("?", |s| s.as_str())
329    }
330
331    /// Format kind without text content.
332    ///
333    /// - Named nodes: `kind` (e.g., `identifier`)
334    /// - Anonymous nodes: `kind` dim green (e.g., `let`)
335    fn format_kind_simple(&self, kind: &str, is_named: bool) -> String {
336        if is_named {
337            kind.to_string()
338        } else {
339            let c = &self.colors;
340            format!("{}{}{}{}", c.dim, c.green, kind, c.reset)
341        }
342    }
343
344    /// Format kind with source text, dynamically truncated to fit content width.
345    ///
346    /// - Named nodes: `kind text` (e.g., `identifier fetchData`)
347    /// - Anonymous nodes: just `text` in green (kind == text, no redundancy)
348    fn format_kind_with_text(&self, kind: &str, text: &str, is_named: bool) -> String {
349        let c = &self.colors;
350
351        // Available content width = TOTAL_WIDTH - prefix_width + step_width
352        // prefix_width = INDENT + step_width + GAP + SYMBOL + GAP = 9 + step_width
353        // +step_width because ellipsis can extend into the successors column
354        // (sub-lines have no successors, so we use that space)
355        // This simplifies to: TOTAL_WIDTH - 9 = 35
356        let available = cols::TOTAL_WIDTH - 9;
357
358        if is_named {
359            // Named: show kind + text
360            let text_budget = available.saturating_sub(kind.len() + 1).max(12);
361            let truncated = truncate_text(text, text_budget);
362            format!("{} {}{}{}{}", kind, c.dim, c.green, truncated, c.reset)
363        } else {
364            // Anonymous: just text dim green (kind == text, no redundancy)
365            let truncated = truncate_text(text, available);
366            format!("{}{}{}{}", c.dim, c.green, truncated, c.reset)
367        }
368    }
369
370    /// Format a runtime effect for display.
371    fn format_effect(&self, effect: &RuntimeEffect<'_>) -> String {
372        use RuntimeEffect::*;
373        match effect {
374            Node(_) => "Node".to_string(),
375            Text(_) => "Text".to_string(),
376            Arr => "Arr".to_string(),
377            Push => "Push".to_string(),
378            EndArr => "EndArr".to_string(),
379            Obj => "Obj".to_string(),
380            EndObj => "EndObj".to_string(),
381            Set(idx) => format!("Set \"{}\"", self.member_name(*idx)),
382            Enum(idx) => format!("Enum \"{}\"", self.member_name(*idx)),
383            EndEnum => "EndEnum".to_string(),
384            Clear => "Clear".to_string(),
385            Null => "Null".to_string(),
386        }
387    }
388
389    /// Format a suppressed effect from opcode and payload.
390    fn format_effect_from_opcode(&self, opcode: EffectOpcode, payload: usize) -> String {
391        use EffectOpcode::*;
392        match opcode {
393            Node => "Node".to_string(),
394            Text => "Text".to_string(),
395            Arr => "Arr".to_string(),
396            Push => "Push".to_string(),
397            EndArr => "EndArr".to_string(),
398            Obj => "Obj".to_string(),
399            EndObj => "EndObj".to_string(),
400            Set => format!("Set \"{}\"", self.member_name(payload as u16)),
401            Enum => format!("Enum \"{}\"", self.member_name(payload as u16)),
402            EndEnum => "EndEnum".to_string(),
403            Clear => "Clear".to_string(),
404            Null => "Null".to_string(),
405            SuppressBegin | SuppressEnd => unreachable!(),
406        }
407    }
408
409    /// Format match content for instruction line (matches dump format exactly).
410    ///
411    /// Order: [pre-effects] !neg_fields field: (type) predicate [post-effects]
412    fn format_match_content(&self, m: &Match<'_>) -> String {
413        let mut parts = Vec::new();
414
415        // Pre-effects: [Effect1 Effect2]
416        let pre: Vec<_> = m.pre_effects().map(|e| format_effect(&e)).collect();
417        if !pre.is_empty() {
418            parts.push(format!("[{}]", pre.join(" ")));
419        }
420
421        // Skip neg_fields and node pattern for epsilon (no node interaction)
422        if !m.is_epsilon() {
423            // Negated fields: !field1 !field2
424            for field_id in m.neg_fields() {
425                let name = self.node_field_name(field_id);
426                parts.push(format!("!{name}"));
427            }
428
429            // Node pattern: field: (type) / (type) / field: _ / empty
430            let node_part = self.format_node_pattern(m);
431            if !node_part.is_empty() {
432                parts.push(node_part);
433            }
434
435            // Predicate: == "value" or =~ /pattern/
436            if let Some((op, is_regex, value_ref)) = m.predicate() {
437                let op = PredicateOp::from_byte(op);
438                let value = if is_regex {
439                    let pattern = &self.regex_patterns[value_ref as usize];
440                    format!("/{}/", pattern)
441                } else {
442                    let s = &self.all_strings[value_ref as usize];
443                    format!("{:?}", s)
444                };
445                parts.push(format!("{} {}", op.as_str(), value));
446            }
447        }
448
449        // Post-effects: [Effect1 Effect2]
450        let post: Vec<_> = m.post_effects().map(|e| format_effect(&e)).collect();
451        if !post.is_empty() {
452            parts.push(format!("[{}]", post.join(" ")));
453        }
454
455        parts.join(" ")
456    }
457
458    /// Format node pattern: `field: (type)` or `(type)` or `field: _` or `"text"` or empty.
459    fn format_node_pattern(&self, m: &Match<'_>) -> String {
460        let mut result = String::new();
461
462        if let Some(f) = m.node_field {
463            result.push_str(self.node_field_name(f.get()));
464            result.push_str(": ");
465        }
466
467        match m.node_type {
468            NodeTypeIR::Any => {
469                // Any node wildcard: `_`
470                result.push('_');
471            }
472            NodeTypeIR::Named(None) => {
473                // Named wildcard: any named node
474                result.push_str("(_)");
475            }
476            NodeTypeIR::Named(Some(t)) => {
477                // Specific named node type
478                result.push('(');
479                result.push_str(self.node_type_name(t.get()));
480                result.push(')');
481            }
482            NodeTypeIR::Anonymous(None) => {
483                // Anonymous wildcard: any anonymous node
484                result.push_str("\"_\"");
485            }
486            NodeTypeIR::Anonymous(Some(t)) => {
487                // Specific anonymous node (literal token)
488                result.push('"');
489                result.push_str(self.node_type_name(t.get()));
490                result.push('"');
491            }
492        }
493
494        result
495    }
496
497    /// Print all trace lines.
498    pub fn print(&self) {
499        for line in &self.lines {
500            println!("{}", line);
501        }
502    }
503
504    /// Add an instruction line.
505    fn add_instruction(&mut self, ip: u16, symbol: Symbol, content: &str, successors: &str) {
506        let prefix = format!("  {:0sw$} {} ", ip, symbol.format(), sw = self.step_width);
507        let line = self
508            .builder
509            .pad_successors(format!("{prefix}{content}"), successors);
510        self.lines.push(line);
511    }
512
513    /// Add a sub-line (blank step area + symbol + content).
514    fn add_subline(&mut self, symbol: Symbol, content: &str) {
515        let step_area = 2 + self.step_width + 1;
516        let prefix = format!("{:step_area$}{} ", "", symbol.format());
517        self.lines.push(format!("{prefix}{content}"));
518    }
519
520    /// Format definition name (blue). User definitions get parentheses, preamble doesn't.
521    fn format_def_name(&self, name: &str) -> String {
522        let c = self.colors;
523        if name.starts_with('_') {
524            // Preamble/internal names: no parentheses
525            format!("{}{}{}", c.blue, name, c.reset)
526        } else {
527            // User definitions: wrap in parentheses
528            format!("({}{}{})", c.blue, name, c.reset)
529        }
530    }
531
532    /// Format definition label with colon (blue).
533    fn format_def_label(&self, name: &str) -> String {
534        let c = self.colors;
535        format!("{}{}{}:", c.blue, name, c.reset)
536    }
537
538    /// Push a definition label, with empty line separator (except for first label).
539    fn push_def_label(&mut self, name: &str) {
540        if !self.lines.is_empty() {
541            self.lines.push(String::new());
542        }
543        self.lines.push(self.format_def_label(name));
544    }
545
546    /// Format cache line boundary separator (full-width dashes, dimmed).
547    fn format_cache_line_separator(&self) -> String {
548        // Content spans TOTAL_WIDTH (same as instruction lines before successors)
549        let c = self.colors;
550        format!(
551            "{:indent$}{}{}{}",
552            "",
553            c.dim,
554            "-".repeat(cols::TOTAL_WIDTH),
555            c.reset,
556            indent = cols::INDENT,
557        )
558    }
559
560    /// Check if IPs cross a cache line boundary and insert separator if so.
561    ///
562    /// Cache line = 64 bytes = 8 steps (each step is 8 bytes).
563    /// Only shows separator in verbose modes (-v, -vv).
564    fn check_cache_line_boundary(&mut self, ip: u16) {
565        // Only show cache line separators in verbose modes
566        if self.verbosity == Verbosity::Default {
567            self.prev_ip = Some(ip);
568            return;
569        }
570
571        const STEPS_PER_CACHE_LINE: u16 = 8;
572        if let Some(prev) = self.prev_ip
573            && prev / STEPS_PER_CACHE_LINE != ip / STEPS_PER_CACHE_LINE
574        {
575            self.lines.push(self.format_cache_line_separator());
576        }
577        self.prev_ip = Some(ip);
578    }
579}
580
581impl Tracer for PrintTracer<'_> {
582    fn trace_instruction(&mut self, ip: u16, instr: &Instruction<'_>) {
583        // Check for cache line boundary crossing
584        self.check_cache_line_boundary(ip);
585
586        match instr {
587            Instruction::Match(m) => {
588                // Show ε for epsilon transitions, empty otherwise (nav shown in sublines)
589                let symbol = if m.is_epsilon() {
590                    Symbol::EPSILON
591                } else {
592                    Symbol::EMPTY
593                };
594                let content = self.format_match_content(m);
595                let successors = format_match_successors(m);
596                self.add_instruction(ip, symbol, &content, &successors);
597            }
598            Instruction::Call(c) => {
599                let name = self.entrypoint_name(c.target.get());
600                let content = self.format_def_name(name);
601                let successors = format!("{:02} : {:02}", c.target.get(), c.next.get());
602                self.add_instruction(ip, Symbol::EMPTY, &content, &successors);
603            }
604            Instruction::Return(_) => {
605                self.pending_return_ip = Some(ip);
606            }
607            Instruction::Trampoline(t) => {
608                // Trampoline shows as a call to the entrypoint target
609                let content = "Trampoline";
610                let successors = format!("{:02}", t.next.get());
611                self.add_instruction(ip, Symbol::EMPTY, content, &successors);
612            }
613        }
614    }
615
616    fn trace_nav(&mut self, nav: Nav, node: Node<'_>) {
617        // Navigation sub-lines hidden in default verbosity
618        if self.verbosity == Verbosity::Default {
619            return;
620        }
621
622        let kind = node.kind();
623        let symbol = match nav {
624            Nav::Epsilon => Symbol::EPSILON,
625            Nav::Down | Nav::DownSkip | Nav::DownExact => trace::NAV_DOWN,
626            Nav::Next | Nav::NextSkip | Nav::NextExact => trace::NAV_NEXT,
627            Nav::Up(_) | Nav::UpSkipTrivia(_) | Nav::UpExact(_) => trace::NAV_UP,
628            Nav::Stay | Nav::StayExact => Symbol::EMPTY,
629        };
630
631        // Text only in VeryVerbose
632        if self.verbosity == Verbosity::VeryVerbose {
633            let text = node.utf8_text(self.source).unwrap_or("?");
634            let content = self.format_kind_with_text(kind, text, node.is_named());
635            self.add_subline(symbol, &content);
636        } else {
637            let content = self.format_kind_simple(kind, node.is_named());
638            self.add_subline(symbol, &content);
639        }
640    }
641
642    fn trace_nav_failure(&mut self, nav: Nav) {
643        // Navigation failure sub-lines hidden in default verbosity
644        if self.verbosity == Verbosity::Default {
645            return;
646        }
647
648        // Show the failed navigation direction
649        let nav_symbol = match nav {
650            Nav::Down | Nav::DownSkip | Nav::DownExact => "▽",
651            Nav::Next | Nav::NextSkip | Nav::NextExact => "▷",
652            Nav::Up(_) | Nav::UpSkipTrivia(_) | Nav::UpExact(_) => "△",
653            Nav::Stay | Nav::StayExact | Nav::Epsilon => "·",
654        };
655
656        self.add_subline(trace::MATCH_FAILURE, nav_symbol);
657    }
658
659    fn trace_match_success(&mut self, node: Node<'_>) {
660        let kind = node.kind();
661
662        // Text on match/failure in Verbose+
663        if self.verbosity != Verbosity::Default {
664            let text = node.utf8_text(self.source).unwrap_or("?");
665            let content = self.format_kind_with_text(kind, text, node.is_named());
666            self.add_subline(trace::MATCH_SUCCESS, &content);
667        } else {
668            let content = self.format_kind_simple(kind, node.is_named());
669            self.add_subline(trace::MATCH_SUCCESS, &content);
670        }
671    }
672
673    fn trace_match_failure(&mut self, node: Node<'_>) {
674        let kind = node.kind();
675
676        // Text on match/failure in Verbose+
677        if self.verbosity != Verbosity::Default {
678            let text = node.utf8_text(self.source).unwrap_or("?");
679            let content = self.format_kind_with_text(kind, text, node.is_named());
680            self.add_subline(trace::MATCH_FAILURE, &content);
681        } else {
682            let content = self.format_kind_simple(kind, node.is_named());
683            self.add_subline(trace::MATCH_FAILURE, &content);
684        }
685    }
686
687    fn trace_field_success(&mut self, field_id: NonZeroU16) {
688        // Field success sub-lines hidden in default verbosity
689        if self.verbosity == Verbosity::Default {
690            return;
691        }
692
693        let name = self.node_field_name(field_id.get());
694        self.add_subline(trace::MATCH_SUCCESS, &format!("{}:", name));
695    }
696
697    fn trace_field_failure(&mut self, _node: Node<'_>) {
698        // Field failures are silent - we just backtrack
699    }
700
701    fn trace_effect(&mut self, effect: &RuntimeEffect<'_>) {
702        // Effect sub-lines hidden in default verbosity
703        if self.verbosity == Verbosity::Default {
704            return;
705        }
706
707        let effect_str = self.format_effect(effect);
708        self.add_subline(trace::EFFECT, &effect_str);
709    }
710
711    fn trace_effect_suppressed(&mut self, opcode: EffectOpcode, payload: usize) {
712        // Effect sub-lines hidden in default verbosity
713        if self.verbosity == Verbosity::Default {
714            return;
715        }
716
717        let effect_str = self.format_effect_from_opcode(opcode, payload);
718        self.add_subline(trace::EFFECT_SUPPRESSED, &effect_str);
719    }
720
721    fn trace_suppress_control(&mut self, opcode: EffectOpcode, suppressed: bool) {
722        // Effect sub-lines hidden in default verbosity
723        if self.verbosity == Verbosity::Default {
724            return;
725        }
726
727        let name = match opcode {
728            EffectOpcode::SuppressBegin => "SuppressBegin",
729            EffectOpcode::SuppressEnd => "SuppressEnd",
730            _ => unreachable!(),
731        };
732        let symbol = if suppressed {
733            trace::EFFECT_SUPPRESSED
734        } else {
735            trace::EFFECT
736        };
737        self.add_subline(symbol, name);
738    }
739
740    fn trace_call(&mut self, target_ip: u16) {
741        let name = self.entrypoint_name(target_ip).to_string();
742        self.add_subline(trace::CALL, &self.format_def_name(&name));
743        self.push_def_label(&name);
744        self.definition_stack.push(name);
745    }
746
747    fn trace_return(&mut self) {
748        let ip = self
749            .pending_return_ip
750            .take()
751            .expect("trace_return without trace_instruction");
752        let name = self
753            .definition_stack
754            .pop()
755            .expect("trace_return requires balanced call stack");
756        let content = self.format_def_name(&name);
757        // Show ◼ when returning from top-level (stack now empty)
758        let is_top_level = self.definition_stack.is_empty();
759        let successor = if is_top_level { "◼" } else { "" };
760        self.add_instruction(ip, trace::RETURN, &content, successor);
761        // Print caller's label after return (if not top-level)
762        if let Some(caller) = self.definition_stack.last().cloned() {
763            self.push_def_label(&caller);
764        }
765    }
766
767    fn trace_checkpoint_created(&mut self, ip: u16) {
768        self.checkpoint_ips.push(ip);
769    }
770
771    fn trace_backtrack(&mut self) {
772        let created_at = self
773            .checkpoint_ips
774            .pop()
775            .expect("backtrack without checkpoint");
776        let line = format!(
777            "  {:0sw$} {}",
778            created_at,
779            trace::BACKTRACK.format(),
780            sw = self.step_width
781        );
782        self.lines.push(line);
783    }
784
785    fn trace_enter_entrypoint(&mut self, target_ip: u16) {
786        let name = self.entrypoint_name(target_ip).to_string();
787        self.push_def_label(&name);
788        self.definition_stack.push(name);
789    }
790
791    fn trace_enter_preamble(&mut self) {
792        const PREAMBLE_NAME: &str = "_ObjWrap";
793        self.push_def_label(PREAMBLE_NAME);
794        self.definition_stack.push(PREAMBLE_NAME.to_string());
795    }
796}
797
798/// Format match successors for instruction line.
799fn format_match_successors(m: &Match<'_>) -> String {
800    if m.is_terminal() {
801        "◼".to_string()
802    } else if m.succ_count() == 1 {
803        format!("{:02}", m.successor(0).get())
804    } else {
805        let succs: Vec<_> = m.successors().map(|s| format!("{:02}", s.get())).collect();
806        succs.join(", ")
807    }
808}