From c182de4fb5f2a7fe3386e062794f1c131f47d38d Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 08:46:53 -0300 Subject: [PATCH 1/2] refactor: add colors module for terminal output --- crates/plotnik-lib/src/colors.rs | 57 ++++++++++++++++++++++++++++++++ crates/plotnik-lib/src/lib.rs | 2 ++ 2 files changed, 59 insertions(+) create mode 100644 crates/plotnik-lib/src/colors.rs diff --git a/crates/plotnik-lib/src/colors.rs b/crates/plotnik-lib/src/colors.rs new file mode 100644 index 00000000..644f43e3 --- /dev/null +++ b/crates/plotnik-lib/src/colors.rs @@ -0,0 +1,57 @@ +//! ANSI color codes for terminal output. +//! +//! Four semantic colors with orthogonal dim modifier: +//! - Blue: Definition names, keys, type names +//! - Green: String literals, terminal markers +//! - Dim: Structure, nav, effects, metadata +//! - Reset: Return to default + +/// ANSI color palette for CLI output. +/// +/// Designed for jq-inspired colorization that works in both light and dark themes. +/// Uses only standard 16-color ANSI codes (no RGB). +#[derive(Clone, Copy, Debug)] +pub struct Colors { + pub blue: &'static str, + pub green: &'static str, + pub dim: &'static str, + pub reset: &'static str, +} + +impl Default for Colors { + fn default() -> Self { + Self::OFF + } +} + +impl Colors { + /// Colors enabled (ANSI escape codes). + pub const ON: Self = Self { + blue: "\x1b[34m", + green: "\x1b[32m", + dim: "\x1b[2m", + reset: "\x1b[0m", + }; + + /// Colors disabled (empty strings). + pub const OFF: Self = Self { + blue: "", + green: "", + dim: "", + reset: "", + }; + + /// Create colors based on enabled flag. + pub fn new(enabled: bool) -> Self { + if enabled { + Self::ON + } else { + Self::OFF + } + } + + /// Check if colors are enabled. + pub fn is_enabled(&self) -> bool { + !self.blue.is_empty() + } +} diff --git a/crates/plotnik-lib/src/lib.rs b/crates/plotnik-lib/src/lib.rs index 05278d97..cdf99a45 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -18,6 +18,7 @@ pub mod analyze; pub mod bytecode; +pub mod colors; pub mod compile; pub mod diagnostics; pub mod emit; @@ -32,6 +33,7 @@ pub mod typegen; /// Fatal errors (like fuel exhaustion) use the outer `Result`. pub type PassResult = std::result::Result<(T, Diagnostics), Error>; +pub use colors::Colors; pub use diagnostics::{Diagnostics, DiagnosticsPrinter, Severity, Span}; pub use query::{Query, QueryBuilder}; pub use query::{SourceId, SourceMap}; From bd69383077cf550f3b0148d72f6703e44f19039b Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 08:52:48 -0300 Subject: [PATCH 2/2] refactor: add bytecode format module for shared formatting --- crates/plotnik-lib/src/bytecode/format.rs | 347 ++++++++++++++++++++++ crates/plotnik-lib/src/bytecode/mod.rs | 6 + 2 files changed, 353 insertions(+) create mode 100644 crates/plotnik-lib/src/bytecode/format.rs diff --git a/crates/plotnik-lib/src/bytecode/format.rs b/crates/plotnik-lib/src/bytecode/format.rs new file mode 100644 index 00000000..ccb8b836 --- /dev/null +++ b/crates/plotnik-lib/src/bytecode/format.rs @@ -0,0 +1,347 @@ +//! Shared formatting utilities for bytecode dump and execution trace. +//! +//! Both dump and trace use the same column layout: +//! ```text +//! | 2 | step | 1 | 5 | 1 | content | 1 | succ | +//! | | pad | | (sym) | | | | | +//! ``` + +use super::effects::EffectOpcode; +use super::nav::Nav; +use super::EffectOp; + +// ============================================================================= +// Column Layout +// ============================================================================= + +/// Column widths for instruction line formatting. +pub mod cols { + /// Leading indentation (2 spaces). + pub const INDENT: usize = 2; + /// Gap between columns (1 space). + pub const GAP: usize = 1; + /// Symbol column width (5 chars: 2 left + 1 center + 2 right). + pub const SYMBOL: usize = 5; + /// Total width before successors are right-aligned. + pub const TOTAL_WIDTH: usize = 44; +} + +// ============================================================================= +// Symbol Types +// ============================================================================= + +/// Symbols for the 5-character symbol column. +/// +/// Format: `| left(2) | center(1) | right(2) |` +/// +/// Used in both dump (nav symbols) and trace (nav, match, effect symbols). +#[derive(Clone, Copy, Debug)] +pub struct Symbol { + /// Left modifier (2 chars): mode indicator or spaces. + /// Examples: " ", " !", " ‼" + pub left: &'static str, + /// Center symbol (1 char): direction or status. + /// Examples: "ε", "▽", "▷", "△", "●", "○", "⬥", "▶", "◀" + pub center: &'static str, + /// Right suffix (2 chars): level or spaces. + /// Examples: " ", "¹ ", "¹²" + pub right: &'static str, +} + +impl Default for Symbol { + fn default() -> Self { + Self::EMPTY + } +} + +impl Symbol { + /// Create a new symbol with all parts. + pub const fn new(left: &'static str, center: &'static str, right: &'static str) -> Self { + Self { + left, + center, + right, + } + } + + /// Empty symbol (5 spaces). + pub const EMPTY: Symbol = Symbol::new(" ", " ", " "); + + /// Epsilon symbol for unconditional transitions. + pub const EPSILON: Symbol = Symbol::new(" ", "ε", " "); + + /// Format as a 5-character string. + pub fn format(&self) -> String { + format!("{}{}{}", self.left, self.center, self.right) + } +} + +// ============================================================================= +// Navigation Symbols +// ============================================================================= + +/// Format navigation command as a Symbol using the doc-specified triangles. +/// +/// | Nav | Symbol | Notes | +/// | --------------- | ------- | ----------------------------------- | +/// | Stay | (blank) | No movement, 5 spaces | +/// | Stay (epsilon) | ε | Only when no type/field constraints | +/// | Down | ▽ | First child, skip any | +/// | DownSkip | !▽ | First child, skip trivia | +/// | DownExact | ‼▽ | First child, exact | +/// | Next | ▷ | Next sibling, skip any | +/// | NextSkip | !▷ | Next sibling, skip trivia | +/// | NextExact | ‼▷ | Next sibling, exact | +/// | Up(n) | △ⁿ | Ascend n levels, skip any | +/// | UpSkipTrivia(n) | !△ⁿ | Ascend n, must be last non-trivia | +/// | UpExact(n) | ‼△ⁿ | Ascend n, must be last child | +pub fn nav_symbol(nav: Nav) -> Symbol { + match nav { + Nav::Stay => Symbol::EMPTY, + Nav::Down => Symbol::new(" ", "▽", " "), + Nav::DownSkip => Symbol::new(" !", "▽", " "), + Nav::DownExact => Symbol::new(" ‼", "▽", " "), + Nav::Next => Symbol::new(" ", "▷", " "), + Nav::NextSkip => Symbol::new(" !", "▷", " "), + Nav::NextExact => Symbol::new(" ‼", "▷", " "), + Nav::Up(n) => Symbol::new(" ", "△", superscript_suffix(n)), + Nav::UpSkipTrivia(n) => Symbol::new(" !", "△", superscript_suffix(n)), + Nav::UpExact(n) => Symbol::new(" ‼", "△", superscript_suffix(n)), + } +} + +/// Format navigation for epsilon transitions (when is_epsilon is true). +/// +/// True epsilon transitions require all three conditions: +/// - `nav == Stay` (no cursor movement) +/// - `node_type == None` (no type constraint) +/// - `node_field == None` (no field constraint) +pub fn nav_symbol_epsilon(nav: Nav, is_epsilon: bool) -> Symbol { + if is_epsilon { + Symbol::EPSILON + } else { + nav_symbol(nav) + } +} + +// ============================================================================= +// Trace-Specific Symbols +// ============================================================================= + +/// Trace sub-line symbols. +pub mod trace { + use super::Symbol; + + /// Navigation: descended to child. + pub const NAV_DOWN: Symbol = Symbol::new(" ", "▽", " "); + /// Navigation: moved to sibling. + pub const NAV_NEXT: Symbol = Symbol::new(" ", "▷", " "); + /// Navigation: ascended to parent. + pub const NAV_UP: Symbol = Symbol::new(" ", "△", " "); + + /// Match: success. + pub const MATCH_SUCCESS: Symbol = Symbol::new(" ", "●", " "); + /// Match: failure. + pub const MATCH_FAILURE: Symbol = Symbol::new(" ", "○", " "); + + /// Effect: data capture or structure. + pub const EFFECT: Symbol = Symbol::new(" ", "⬥", " "); + + /// Call: entering definition. + pub const CALL: Symbol = Symbol::new(" ", "▶", " "); + /// Return: back from definition. + pub const RETURN: Symbol = Symbol::new(" ", "◀", " "); + + /// Backtrack symbol (centered in 5 chars). + pub const BACKTRACK: Symbol = Symbol::new(" ", "❮❮❮", " "); +} + +// ============================================================================= +// Superscript Formatting +// ============================================================================= + +const SUPERSCRIPT_DIGITS: &[char] = &['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; + +/// Convert a number to superscript digits. +pub fn superscript(n: u8) -> String { + if n < 10 { + SUPERSCRIPT_DIGITS[n as usize].to_string() + } else { + n.to_string() + .chars() + .map(|c| SUPERSCRIPT_DIGITS[c.to_digit(10).unwrap() as usize]) + .collect() + } +} + +/// Convert a number to a 2-char superscript suffix for the symbol right column. +/// Level 1 shows no superscript (blank), levels 2+ show superscript. +fn superscript_suffix(n: u8) -> &'static str { + match n { + 1 => " ", + 2 => "² ", + 3 => "³ ", + 4 => "⁴ ", + 5 => "⁵ ", + 6 => "⁶ ", + 7 => "⁷ ", + 8 => "⁸ ", + 9 => "⁹ ", + // For 10+, we'd need dynamic allocation. Rare in practice. + _ => "ⁿ ", + } +} + +// ============================================================================= +// Width Calculation +// ============================================================================= + +/// Calculate minimum width needed to display numbers up to `count - 1`. +pub fn width_for_count(count: usize) -> usize { + if count <= 1 { + 1 + } else { + ((count - 1) as f64).log10().floor() as usize + 1 + } +} + +// ============================================================================= +// Text Truncation +// ============================================================================= + +/// Truncate text to max length with ellipsis. +/// +/// Used for displaying node text in traces. +pub fn truncate_text(s: &str, max_len: usize) -> String { + if s.chars().count() <= max_len { + s.to_string() + } else { + let truncated: String = s.chars().take(max_len - 1).collect(); + format!("{}…", truncated) + } +} + +// ============================================================================= +// Line Building +// ============================================================================= + +/// Builder for formatted output lines. +/// +/// Constructs lines following the column layout: +/// `...` +pub struct LineBuilder { + step_width: usize, +} + +impl LineBuilder { + /// Create a new line builder with the given step width. + pub fn new(step_width: usize) -> Self { + Self { step_width } + } + + /// Build an instruction line prefix: ` ` + pub fn instruction_prefix(&self, step: u16, symbol: Symbol) -> String { + format!( + "{:indent$}{:0sw$} {} ", + "", + step, + symbol.format(), + indent = cols::INDENT, + sw = self.step_width, + ) + } + + /// Build a sub-line prefix (blank step area): ` ` + pub fn subline_prefix(&self, symbol: Symbol) -> String { + let step_area = cols::INDENT + self.step_width + cols::GAP; + format!("{:step_area$}{} ", "", symbol.format()) + } + + /// Build a backtrack line: ` ❮❮❮` + pub fn backtrack_line(&self, step: u16) -> String { + format!( + "{:indent$}{:0sw$} {}", + "", + step, + trace::BACKTRACK.format(), + indent = cols::INDENT, + sw = self.step_width, + ) + } + + /// Pad content to total width and append successors. + /// + /// Ensures at least 2 spaces between content and successors. + pub fn pad_successors(&self, base: String, successors: &str) -> String { + let padding = cols::TOTAL_WIDTH.saturating_sub(base.chars().count()).max(2); + format!("{base}{:padding$}{successors}", "") + } +} + +// ============================================================================= +// Effect Formatting +// ============================================================================= + +/// Format an effect operation for display. +pub fn format_effect(effect: &EffectOp) -> String { + match effect.opcode { + EffectOpcode::Node => "Node".to_string(), + EffectOpcode::Arr => "Arr".to_string(), + EffectOpcode::Push => "Push".to_string(), + EffectOpcode::EndArr => "EndArr".to_string(), + EffectOpcode::Obj => "Obj".to_string(), + EffectOpcode::EndObj => "EndObj".to_string(), + EffectOpcode::Set => format!("Set(M{})", effect.payload), + EffectOpcode::Enum => format!("Enum(M{})", effect.payload), + EffectOpcode::EndEnum => "EndEnum".to_string(), + EffectOpcode::Text => "Text".to_string(), + EffectOpcode::Clear => "Clear".to_string(), + EffectOpcode::Null => "Null".to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_symbol_format() { + assert_eq!(Symbol::EMPTY.format(), " "); + assert_eq!(Symbol::EPSILON.format(), " ε "); + assert_eq!(nav_symbol(Nav::Down).format(), " ▽ "); + assert_eq!(nav_symbol(Nav::DownSkip).format(), " !▽ "); + assert_eq!(nav_symbol(Nav::DownExact).format(), " ‼▽ "); + assert_eq!(nav_symbol(Nav::Next).format(), " ▷ "); + assert_eq!(nav_symbol(Nav::Up(1)).format(), " △ "); + assert_eq!(nav_symbol(Nav::Up(2)).format(), " △² "); + assert_eq!(nav_symbol(Nav::UpSkipTrivia(1)).format(), " !△ "); + assert_eq!(nav_symbol(Nav::UpExact(1)).format(), " ‼△ "); + } + + #[test] + fn test_width_for_count() { + assert_eq!(width_for_count(0), 1); + assert_eq!(width_for_count(1), 1); + assert_eq!(width_for_count(10), 1); + assert_eq!(width_for_count(11), 2); + assert_eq!(width_for_count(100), 2); + assert_eq!(width_for_count(101), 3); + } + + #[test] + fn test_truncate_text() { + assert_eq!(truncate_text("hello", 10), "hello"); + assert_eq!(truncate_text("hello world", 10), "hello wor…"); + assert_eq!(truncate_text("abc", 3), "abc"); + assert_eq!(truncate_text("abcd", 3), "ab…"); + } + + #[test] + fn test_superscript() { + assert_eq!(superscript(0), "⁰"); + assert_eq!(superscript(1), "¹"); + assert_eq!(superscript(9), "⁹"); + assert_eq!(superscript(10), "¹⁰"); + assert_eq!(superscript(12), "¹²"); + } +} diff --git a/crates/plotnik-lib/src/bytecode/mod.rs b/crates/plotnik-lib/src/bytecode/mod.rs index 472b972c..abe0f8a8 100644 --- a/crates/plotnik-lib/src/bytecode/mod.rs +++ b/crates/plotnik-lib/src/bytecode/mod.rs @@ -6,6 +6,7 @@ mod constants; mod dump; mod effects; mod entrypoint; +mod format; mod header; mod ids; mod instructions; @@ -42,6 +43,11 @@ pub use module::{ pub use dump::dump; +pub use format::{ + cols, format_effect, nav_symbol, nav_symbol_epsilon, superscript, trace, truncate_text, + width_for_count, LineBuilder, Symbol, +}; + #[cfg(test)] mod instructions_tests; #[cfg(test)]