use std::num::NonZeroU16;
use arborium_tree_sitter::Node;
use plotnik_bytecode::{
EffectOpcode, Instruction, LineBuilder, Match, Module, Nav, NodeTypeIR, PredicateOp, Symbol,
cols, format_effect, trace, truncate_text, width_for_count,
};
use plotnik_core::Colors;
use super::effect::RuntimeEffect;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Verbosity {
#[default]
Default,
Verbose,
VeryVerbose,
}
pub trait Tracer {
fn trace_instruction(&mut self, ip: u16, instr: &Instruction<'_>);
fn trace_nav(&mut self, nav: Nav, node: Node<'_>);
fn trace_nav_failure(&mut self, nav: Nav);
fn trace_match_success(&mut self, node: Node<'_>);
fn trace_match_failure(&mut self, node: Node<'_>);
fn trace_field_success(&mut self, field_id: NonZeroU16);
fn trace_field_failure(&mut self, node: Node<'_>);
fn trace_effect(&mut self, effect: &RuntimeEffect<'_>);
fn trace_effect_suppressed(&mut self, opcode: EffectOpcode, payload: usize);
fn trace_suppress_control(&mut self, opcode: EffectOpcode, suppressed: bool);
fn trace_call(&mut self, target_ip: u16);
fn trace_return(&mut self);
fn trace_checkpoint_created(&mut self, ip: u16);
fn trace_backtrack(&mut self);
fn trace_enter_entrypoint(&mut self, target_ip: u16);
fn trace_enter_preamble(&mut self);
}
pub struct NoopTracer;
impl Tracer for NoopTracer {
#[inline(always)]
fn trace_instruction(&mut self, _ip: u16, _instr: &Instruction<'_>) {}
#[inline(always)]
fn trace_nav(&mut self, _nav: Nav, _node: Node<'_>) {}
#[inline(always)]
fn trace_nav_failure(&mut self, _nav: Nav) {}
#[inline(always)]
fn trace_match_success(&mut self, _node: Node<'_>) {}
#[inline(always)]
fn trace_match_failure(&mut self, _node: Node<'_>) {}
#[inline(always)]
fn trace_field_success(&mut self, _field_id: NonZeroU16) {}
#[inline(always)]
fn trace_field_failure(&mut self, _node: Node<'_>) {}
#[inline(always)]
fn trace_effect(&mut self, _effect: &RuntimeEffect<'_>) {}
#[inline(always)]
fn trace_effect_suppressed(&mut self, _opcode: EffectOpcode, _payload: usize) {}
#[inline(always)]
fn trace_suppress_control(&mut self, _opcode: EffectOpcode, _suppressed: bool) {}
#[inline(always)]
fn trace_call(&mut self, _target_ip: u16) {}
#[inline(always)]
fn trace_return(&mut self) {}
#[inline(always)]
fn trace_checkpoint_created(&mut self, _ip: u16) {}
#[inline(always)]
fn trace_backtrack(&mut self) {}
#[inline(always)]
fn trace_enter_entrypoint(&mut self, _target_ip: u16) {}
#[inline(always)]
fn trace_enter_preamble(&mut self) {}
}
use std::collections::BTreeMap;
pub struct PrintTracer<'s> {
pub(crate) source: &'s [u8],
pub(crate) verbosity: Verbosity,
pub(crate) lines: Vec<String>,
pub(crate) builder: LineBuilder,
pub(crate) node_type_names: BTreeMap<u16, String>,
pub(crate) node_field_names: BTreeMap<u16, String>,
pub(crate) member_names: Vec<String>,
pub(crate) all_strings: Vec<String>,
pub(crate) regex_patterns: Vec<String>,
pub(crate) entrypoint_by_ip: BTreeMap<u16, String>,
pub(crate) checkpoint_ips: Vec<u16>,
pub(crate) definition_stack: Vec<String>,
pub(crate) pending_return_ip: Option<u16>,
pub(crate) step_width: usize,
pub(crate) colors: Colors,
pub(crate) prev_ip: Option<u16>,
}
pub struct PrintTracerBuilder<'s, 'm> {
source: &'s str,
module: &'m Module,
verbosity: Verbosity,
colors: Colors,
}
impl<'s, 'm> PrintTracerBuilder<'s, 'm> {
pub fn new(source: &'s str, module: &'m Module) -> Self {
Self {
source,
module,
verbosity: Verbosity::Default,
colors: Colors::OFF,
}
}
pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
self.verbosity = verbosity;
self
}
pub fn colored(mut self, enabled: bool) -> Self {
self.colors = Colors::new(enabled);
self
}
pub fn build(self) -> PrintTracer<'s> {
let header = self.module.header();
let strings = self.module.strings();
let regexes = self.module.regexes();
let types = self.module.types();
let node_types = self.module.node_types();
let node_fields = self.module.node_fields();
let entrypoints = self.module.entrypoints();
let mut node_type_names = BTreeMap::new();
for i in 0..node_types.len() {
let t = node_types.get(i);
node_type_names.insert(t.id, strings.get(t.name).to_string());
}
let mut node_field_names = BTreeMap::new();
for i in 0..node_fields.len() {
let f = node_fields.get(i);
node_field_names.insert(f.id, strings.get(f.name).to_string());
}
let member_names: Vec<String> = (0..types.members_count())
.map(|i| strings.get(types.get_member(i).name).to_string())
.collect();
let all_strings: Vec<String> = (0..header.str_table_count as usize)
.map(|i| strings.get_by_index(i).to_string())
.collect();
let mut regex_patterns = vec![String::new()];
for i in 1..header.regex_table_count as usize {
let string_id = regexes.get_string_id(i);
regex_patterns.push(strings.get(string_id).to_string());
}
let mut entrypoint_by_ip = BTreeMap::new();
for i in 0..entrypoints.len() {
let e = entrypoints.get(i);
entrypoint_by_ip.insert(e.target, strings.get(e.name).to_string());
}
let step_width = width_for_count(header.transitions_count as usize);
PrintTracer {
source: self.source.as_bytes(),
verbosity: self.verbosity,
lines: Vec::new(),
builder: LineBuilder::new(step_width),
node_type_names,
node_field_names,
member_names,
all_strings,
regex_patterns,
entrypoint_by_ip,
checkpoint_ips: Vec::new(),
definition_stack: Vec::new(),
pending_return_ip: None,
step_width,
colors: self.colors,
prev_ip: None,
}
}
}
impl<'s> PrintTracer<'s> {
pub fn builder<'m>(source: &'s str, module: &'m Module) -> PrintTracerBuilder<'s, 'm> {
PrintTracerBuilder::new(source, module)
}
fn node_type_name(&self, id: u16) -> &str {
self.node_type_names.get(&id).map_or("?", |s| s.as_str())
}
fn node_field_name(&self, id: u16) -> &str {
self.node_field_names.get(&id).map_or("?", |s| s.as_str())
}
fn member_name(&self, idx: u16) -> &str {
self.member_names
.get(idx as usize)
.map_or("?", |s| s.as_str())
}
fn entrypoint_name(&self, ip: u16) -> &str {
self.entrypoint_by_ip.get(&ip).map_or("?", |s| s.as_str())
}
fn format_kind_simple(&self, kind: &str, is_named: bool) -> String {
if is_named {
kind.to_string()
} else {
let c = &self.colors;
format!("{}{}{}{}", c.dim, c.green, kind, c.reset)
}
}
fn format_kind_with_text(&self, kind: &str, text: &str, is_named: bool) -> String {
let c = &self.colors;
let available = cols::TOTAL_WIDTH - 9;
if is_named {
let text_budget = available.saturating_sub(kind.len() + 1).max(12);
let truncated = truncate_text(text, text_budget);
format!("{} {}{}{}{}", kind, c.dim, c.green, truncated, c.reset)
} else {
let truncated = truncate_text(text, available);
format!("{}{}{}{}", c.dim, c.green, truncated, c.reset)
}
}
fn format_effect(&self, effect: &RuntimeEffect<'_>) -> String {
use RuntimeEffect::*;
match effect {
Node(_) => "Node".to_string(),
Text(_) => "Text".to_string(),
Arr => "Arr".to_string(),
Push => "Push".to_string(),
EndArr => "EndArr".to_string(),
Obj => "Obj".to_string(),
EndObj => "EndObj".to_string(),
Set(idx) => format!("Set \"{}\"", self.member_name(*idx)),
Enum(idx) => format!("Enum \"{}\"", self.member_name(*idx)),
EndEnum => "EndEnum".to_string(),
Clear => "Clear".to_string(),
Null => "Null".to_string(),
}
}
fn format_effect_from_opcode(&self, opcode: EffectOpcode, payload: usize) -> String {
use EffectOpcode::*;
match opcode {
Node => "Node".to_string(),
Text => "Text".to_string(),
Arr => "Arr".to_string(),
Push => "Push".to_string(),
EndArr => "EndArr".to_string(),
Obj => "Obj".to_string(),
EndObj => "EndObj".to_string(),
Set => format!("Set \"{}\"", self.member_name(payload as u16)),
Enum => format!("Enum \"{}\"", self.member_name(payload as u16)),
EndEnum => "EndEnum".to_string(),
Clear => "Clear".to_string(),
Null => "Null".to_string(),
SuppressBegin | SuppressEnd => unreachable!(),
}
}
fn format_match_content(&self, m: &Match<'_>) -> String {
let mut parts = Vec::new();
let pre: Vec<_> = m.pre_effects().map(|e| format_effect(&e)).collect();
if !pre.is_empty() {
parts.push(format!("[{}]", pre.join(" ")));
}
if !m.is_epsilon() {
for field_id in m.neg_fields() {
let name = self.node_field_name(field_id);
parts.push(format!("!{name}"));
}
let node_part = self.format_node_pattern(m);
if !node_part.is_empty() {
parts.push(node_part);
}
if let Some((op, is_regex, value_ref)) = m.predicate() {
let op = PredicateOp::from_byte(op);
let value = if is_regex {
let pattern = &self.regex_patterns[value_ref as usize];
format!("/{}/", pattern)
} else {
let s = &self.all_strings[value_ref as usize];
format!("{:?}", s)
};
parts.push(format!("{} {}", op.as_str(), value));
}
}
let post: Vec<_> = m.post_effects().map(|e| format_effect(&e)).collect();
if !post.is_empty() {
parts.push(format!("[{}]", post.join(" ")));
}
parts.join(" ")
}
fn format_node_pattern(&self, m: &Match<'_>) -> String {
let mut result = String::new();
if let Some(f) = m.node_field {
result.push_str(self.node_field_name(f.get()));
result.push_str(": ");
}
match m.node_type {
NodeTypeIR::Any => {
result.push('_');
}
NodeTypeIR::Named(None) => {
result.push_str("(_)");
}
NodeTypeIR::Named(Some(t)) => {
result.push('(');
result.push_str(self.node_type_name(t.get()));
result.push(')');
}
NodeTypeIR::Anonymous(None) => {
result.push_str("\"_\"");
}
NodeTypeIR::Anonymous(Some(t)) => {
result.push('"');
result.push_str(self.node_type_name(t.get()));
result.push('"');
}
}
result
}
pub fn print(&self) {
for line in &self.lines {
println!("{}", line);
}
}
fn add_instruction(&mut self, ip: u16, symbol: Symbol, content: &str, successors: &str) {
let prefix = format!(" {:0sw$} {} ", ip, symbol.format(), sw = self.step_width);
let line = self
.builder
.pad_successors(format!("{prefix}{content}"), successors);
self.lines.push(line);
}
fn add_subline(&mut self, symbol: Symbol, content: &str) {
let step_area = 2 + self.step_width + 1;
let prefix = format!("{:step_area$}{} ", "", symbol.format());
self.lines.push(format!("{prefix}{content}"));
}
fn format_def_name(&self, name: &str) -> String {
let c = self.colors;
if name.starts_with('_') {
format!("{}{}{}", c.blue, name, c.reset)
} else {
format!("({}{}{})", c.blue, name, c.reset)
}
}
fn format_def_label(&self, name: &str) -> String {
let c = self.colors;
format!("{}{}{}:", c.blue, name, c.reset)
}
fn push_def_label(&mut self, name: &str) {
if !self.lines.is_empty() {
self.lines.push(String::new());
}
self.lines.push(self.format_def_label(name));
}
fn format_cache_line_separator(&self) -> String {
let c = self.colors;
format!(
"{:indent$}{}{}{}",
"",
c.dim,
"-".repeat(cols::TOTAL_WIDTH),
c.reset,
indent = cols::INDENT,
)
}
fn check_cache_line_boundary(&mut self, ip: u16) {
if self.verbosity == Verbosity::Default {
self.prev_ip = Some(ip);
return;
}
const STEPS_PER_CACHE_LINE: u16 = 8;
if let Some(prev) = self.prev_ip
&& prev / STEPS_PER_CACHE_LINE != ip / STEPS_PER_CACHE_LINE
{
self.lines.push(self.format_cache_line_separator());
}
self.prev_ip = Some(ip);
}
}
impl Tracer for PrintTracer<'_> {
fn trace_instruction(&mut self, ip: u16, instr: &Instruction<'_>) {
self.check_cache_line_boundary(ip);
match instr {
Instruction::Match(m) => {
let symbol = if m.is_epsilon() {
Symbol::EPSILON
} else {
Symbol::EMPTY
};
let content = self.format_match_content(m);
let successors = format_match_successors(m);
self.add_instruction(ip, symbol, &content, &successors);
}
Instruction::Call(c) => {
let name = self.entrypoint_name(c.target.get());
let content = self.format_def_name(name);
let successors = format!("{:02} : {:02}", c.target.get(), c.next.get());
self.add_instruction(ip, Symbol::EMPTY, &content, &successors);
}
Instruction::Return(_) => {
self.pending_return_ip = Some(ip);
}
Instruction::Trampoline(t) => {
let content = "Trampoline";
let successors = format!("{:02}", t.next.get());
self.add_instruction(ip, Symbol::EMPTY, content, &successors);
}
}
}
fn trace_nav(&mut self, nav: Nav, node: Node<'_>) {
if self.verbosity == Verbosity::Default {
return;
}
let kind = node.kind();
let symbol = match nav {
Nav::Epsilon => Symbol::EPSILON,
Nav::Down | Nav::DownSkip | Nav::DownExact => trace::NAV_DOWN,
Nav::Next | Nav::NextSkip | Nav::NextExact => trace::NAV_NEXT,
Nav::Up(_) | Nav::UpSkipTrivia(_) | Nav::UpExact(_) => trace::NAV_UP,
Nav::Stay | Nav::StayExact => Symbol::EMPTY,
};
if self.verbosity == Verbosity::VeryVerbose {
let text = node.utf8_text(self.source).unwrap_or("?");
let content = self.format_kind_with_text(kind, text, node.is_named());
self.add_subline(symbol, &content);
} else {
let content = self.format_kind_simple(kind, node.is_named());
self.add_subline(symbol, &content);
}
}
fn trace_nav_failure(&mut self, nav: Nav) {
if self.verbosity == Verbosity::Default {
return;
}
let nav_symbol = match nav {
Nav::Down | Nav::DownSkip | Nav::DownExact => "▽",
Nav::Next | Nav::NextSkip | Nav::NextExact => "▷",
Nav::Up(_) | Nav::UpSkipTrivia(_) | Nav::UpExact(_) => "△",
Nav::Stay | Nav::StayExact | Nav::Epsilon => "·",
};
self.add_subline(trace::MATCH_FAILURE, nav_symbol);
}
fn trace_match_success(&mut self, node: Node<'_>) {
let kind = node.kind();
if self.verbosity != Verbosity::Default {
let text = node.utf8_text(self.source).unwrap_or("?");
let content = self.format_kind_with_text(kind, text, node.is_named());
self.add_subline(trace::MATCH_SUCCESS, &content);
} else {
let content = self.format_kind_simple(kind, node.is_named());
self.add_subline(trace::MATCH_SUCCESS, &content);
}
}
fn trace_match_failure(&mut self, node: Node<'_>) {
let kind = node.kind();
if self.verbosity != Verbosity::Default {
let text = node.utf8_text(self.source).unwrap_or("?");
let content = self.format_kind_with_text(kind, text, node.is_named());
self.add_subline(trace::MATCH_FAILURE, &content);
} else {
let content = self.format_kind_simple(kind, node.is_named());
self.add_subline(trace::MATCH_FAILURE, &content);
}
}
fn trace_field_success(&mut self, field_id: NonZeroU16) {
if self.verbosity == Verbosity::Default {
return;
}
let name = self.node_field_name(field_id.get());
self.add_subline(trace::MATCH_SUCCESS, &format!("{}:", name));
}
fn trace_field_failure(&mut self, _node: Node<'_>) {
}
fn trace_effect(&mut self, effect: &RuntimeEffect<'_>) {
if self.verbosity == Verbosity::Default {
return;
}
let effect_str = self.format_effect(effect);
self.add_subline(trace::EFFECT, &effect_str);
}
fn trace_effect_suppressed(&mut self, opcode: EffectOpcode, payload: usize) {
if self.verbosity == Verbosity::Default {
return;
}
let effect_str = self.format_effect_from_opcode(opcode, payload);
self.add_subline(trace::EFFECT_SUPPRESSED, &effect_str);
}
fn trace_suppress_control(&mut self, opcode: EffectOpcode, suppressed: bool) {
if self.verbosity == Verbosity::Default {
return;
}
let name = match opcode {
EffectOpcode::SuppressBegin => "SuppressBegin",
EffectOpcode::SuppressEnd => "SuppressEnd",
_ => unreachable!(),
};
let symbol = if suppressed {
trace::EFFECT_SUPPRESSED
} else {
trace::EFFECT
};
self.add_subline(symbol, name);
}
fn trace_call(&mut self, target_ip: u16) {
let name = self.entrypoint_name(target_ip).to_string();
self.add_subline(trace::CALL, &self.format_def_name(&name));
self.push_def_label(&name);
self.definition_stack.push(name);
}
fn trace_return(&mut self) {
let ip = self
.pending_return_ip
.take()
.expect("trace_return without trace_instruction");
let name = self
.definition_stack
.pop()
.expect("trace_return requires balanced call stack");
let content = self.format_def_name(&name);
let is_top_level = self.definition_stack.is_empty();
let successor = if is_top_level { "◼" } else { "" };
self.add_instruction(ip, trace::RETURN, &content, successor);
if let Some(caller) = self.definition_stack.last().cloned() {
self.push_def_label(&caller);
}
}
fn trace_checkpoint_created(&mut self, ip: u16) {
self.checkpoint_ips.push(ip);
}
fn trace_backtrack(&mut self) {
let created_at = self
.checkpoint_ips
.pop()
.expect("backtrack without checkpoint");
let line = format!(
" {:0sw$} {}",
created_at,
trace::BACKTRACK.format(),
sw = self.step_width
);
self.lines.push(line);
}
fn trace_enter_entrypoint(&mut self, target_ip: u16) {
let name = self.entrypoint_name(target_ip).to_string();
self.push_def_label(&name);
self.definition_stack.push(name);
}
fn trace_enter_preamble(&mut self) {
const PREAMBLE_NAME: &str = "_ObjWrap";
self.push_def_label(PREAMBLE_NAME);
self.definition_stack.push(PREAMBLE_NAME.to_string());
}
}
fn format_match_successors(m: &Match<'_>) -> String {
if m.is_terminal() {
"◼".to_string()
} else if m.succ_count() == 1 {
format!("{:02}", m.successor(0).get())
} else {
let succs: Vec<_> = m.successors().map(|s| format!("{:02}", s.get())).collect();
succs.join(", ")
}
}