1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum Verbosity {
39 #[default]
41 Default,
42 Verbose,
44 VeryVerbose,
46}
47
48pub trait Tracer {
65 fn trace_instruction(&mut self, ip: u16, instr: &Instruction<'_>);
67
68 fn trace_nav(&mut self, nav: Nav, node: Node<'_>);
70
71 fn trace_nav_failure(&mut self, nav: Nav);
73
74 fn trace_match_success(&mut self, node: Node<'_>);
76
77 fn trace_match_failure(&mut self, node: Node<'_>);
79
80 fn trace_field_success(&mut self, field_id: NonZeroU16);
82
83 fn trace_field_failure(&mut self, node: Node<'_>);
85
86 fn trace_effect(&mut self, effect: &RuntimeEffect<'_>);
88
89 fn trace_effect_suppressed(&mut self, opcode: EffectOpcode, payload: usize);
91
92 fn trace_suppress_control(&mut self, opcode: EffectOpcode, suppressed: bool);
95
96 fn trace_call(&mut self, target_ip: u16);
98
99 fn trace_return(&mut self);
101
102 fn trace_checkpoint_created(&mut self, ip: u16);
104
105 fn trace_backtrack(&mut self);
107
108 fn trace_enter_entrypoint(&mut self, target_ip: u16);
110
111 fn trace_enter_preamble(&mut self);
113}
114
115pub 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
170pub struct PrintTracer<'s> {
172 pub(crate) source: &'s [u8],
174 pub(crate) verbosity: Verbosity,
176 pub(crate) lines: Vec<String>,
178 pub(crate) builder: LineBuilder,
180 pub(crate) node_type_names: BTreeMap<u16, String>,
182 pub(crate) node_field_names: BTreeMap<u16, String>,
184 pub(crate) member_names: Vec<String>,
186 pub(crate) all_strings: Vec<String>,
188 pub(crate) regex_patterns: Vec<String>,
190 pub(crate) entrypoint_by_ip: BTreeMap<u16, String>,
192 pub(crate) checkpoint_ips: Vec<u16>,
194 pub(crate) definition_stack: Vec<String>,
196 pub(crate) pending_return_ip: Option<u16>,
198 pub(crate) step_width: usize,
200 pub(crate) colors: Colors,
202 pub(crate) prev_ip: Option<u16>,
204}
205
206pub 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 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 pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
227 self.verbosity = verbosity;
228 self
229 }
230
231 pub fn colored(mut self, enabled: bool) -> Self {
233 self.colors = Colors::new(enabled);
234 self
235 }
236
237 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 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 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 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 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 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 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 fn format_kind_with_text(&self, kind: &str, text: &str, is_named: bool) -> String {
349 let c = &self.colors;
350
351 let available = cols::TOTAL_WIDTH - 9;
357
358 if is_named {
359 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 let truncated = truncate_text(text, available);
366 format!("{}{}{}{}", c.dim, c.green, truncated, c.reset)
367 }
368 }
369
370 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 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 fn format_match_content(&self, m: &Match<'_>) -> String {
413 let mut parts = Vec::new();
414
415 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 if !m.is_epsilon() {
423 for field_id in m.neg_fields() {
425 let name = self.node_field_name(field_id);
426 parts.push(format!("!{name}"));
427 }
428
429 let node_part = self.format_node_pattern(m);
431 if !node_part.is_empty() {
432 parts.push(node_part);
433 }
434
435 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 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 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 result.push('_');
471 }
472 NodeTypeIR::Named(None) => {
473 result.push_str("(_)");
475 }
476 NodeTypeIR::Named(Some(t)) => {
477 result.push('(');
479 result.push_str(self.node_type_name(t.get()));
480 result.push(')');
481 }
482 NodeTypeIR::Anonymous(None) => {
483 result.push_str("\"_\"");
485 }
486 NodeTypeIR::Anonymous(Some(t)) => {
487 result.push('"');
489 result.push_str(self.node_type_name(t.get()));
490 result.push('"');
491 }
492 }
493
494 result
495 }
496
497 pub fn print(&self) {
499 for line in &self.lines {
500 println!("{}", line);
501 }
502 }
503
504 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 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 fn format_def_name(&self, name: &str) -> String {
522 let c = self.colors;
523 if name.starts_with('_') {
524 format!("{}{}{}", c.blue, name, c.reset)
526 } else {
527 format!("({}{}{})", c.blue, name, c.reset)
529 }
530 }
531
532 fn format_def_label(&self, name: &str) -> String {
534 let c = self.colors;
535 format!("{}{}{}:", c.blue, name, c.reset)
536 }
537
538 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 fn format_cache_line_separator(&self) -> String {
548 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 fn check_cache_line_boundary(&mut self, ip: u16) {
565 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 self.check_cache_line_boundary(ip);
585
586 match instr {
587 Instruction::Match(m) => {
588 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 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 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 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 if self.verbosity == Verbosity::Default {
645 return;
646 }
647
648 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 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 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 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 }
700
701 fn trace_effect(&mut self, effect: &RuntimeEffect<'_>) {
702 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 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 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 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 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
798fn 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}