Skip to content

Commit

Permalink
feat: Add an internal parser debugger gated on SLANG_CST_DEBUG
Browse files Browse the repository at this point in the history
  • Loading branch information
Xanewok committed Feb 12, 2024
1 parent 0bfa6b7 commit 22d2513
Show file tree
Hide file tree
Showing 13 changed files with 1,098 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/codegen/parser/generator/src/rust_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl RustGenerator {
"napi_interface/mod.rs",
"parser_support/mod.rs",
"parser_support/context.rs",
"parser_support/debugger.rs",
"parser_support/parser_function.rs",
"parser_support/optional_helper.rs",
"parser_support/sequence_helper.rs",
Expand Down
1 change: 1 addition & 0 deletions crates/codegen/parser/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ariadne = { workspace = true }
napi = { workspace = true, optional = true }
napi-derive = { workspace = true, optional = true }
nom = { workspace = true }
once_cell = { workspace = true }
serde = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/codegen/parser/runtime/src/parser_support/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ impl<'s> ParserContext<'s> {
&self.closing_delimiters
}

pub(super) fn source(&self) -> &str {
self.source
}

pub fn position(&self) -> TextIndex {
self.position
}
Expand Down
216 changes: 216 additions & 0 deletions crates/codegen/parser/runtime/src/parser_support/debugger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//! A simple debugger that logs the executing parser and the current input position.
//! This is helpful in the context of backtracking, as this allows introspection
//! into how much progress a given parser made in general and whether another
//! had to be retried in the event of failure.
use std::fmt;
use std::sync::RwLock;

use once_cell::sync::Lazy;

use crate::kinds::RuleKind;
use crate::parser_support::ParserContext;

pub(crate) static DEBUGGER: Lazy<RwLock<Debugger>> =
Lazy::new(|| RwLock::new(Debugger::with_color().ignore_trivia(true)));

const PREVIEW_LEN: usize = 15;

#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum AnsiColored {
Yes,
#[default]
No,
}

#[derive(Default)]
pub struct Debugger {
enabled: bool,
color: AnsiColored,
ignore_trivia: bool,
parsers: Vec<String>,
}

impl Debugger {
/// Creates an instance of [`Debugger`] that uses ANSI color codes when logging.
fn with_color() -> Self {
Debugger {
enabled: std::env::var("SLANG_CST_DEBUG")
.map_or(false, |v| v != "0" && !v.eq_ignore_ascii_case("false")),
color: AnsiColored::Yes,
ignore_trivia: false,
parsers: Vec::default(),
}
}

/// Whether to ignore trivia when logging.
fn ignore_trivia(mut self, ignore: bool) -> Self {
self.ignore_trivia = ignore;
self
}
}

impl Debugger {
#[allow(dead_code)]
pub fn start_parse(kind: RuleKind, version: impl Into<String>) {
let debugger = DEBUGGER.read().unwrap();
if !debugger.enabled {
return;
}

eprintln!(
"{start_color}Parsing {kind} in version: {version}{end_color}",
version = version.into(),
start_color = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[34m")
.unwrap_or_default(),
end_color = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[0m")
.unwrap_or_default(),
);
}

#[allow(dead_code)]
pub fn enter_parser<'a, 'b>(
context: &'a mut ParserContext<'b>,
parser: impl Into<String>,
) -> EnterParseGuard<'a, 'b> {
let parser_name = parser.into();

if let Ok(debugger) = DEBUGGER.read() {
if !debugger.ignore_trivia || !parser_name.contains("trivia") {
let msg = format!(
"{start}{parser_name}{end}",
start = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[1m")
.unwrap_or_default(),
end = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[0m")
.unwrap_or_default()
);

debugger.log_by_ref(context.preview(), msg);
}
}

DEBUGGER.write().unwrap().parsers.push(parser_name);

EnterParseGuard { context }
}

#[allow(dead_code)]
pub fn log(ctx: &mut ParserContext<'_>, msg: impl fmt::Display) {
DEBUGGER.read().unwrap().log_by_ref(ctx.preview(), msg);
}

fn log_by_ref(&self, preview: SourcePreview, msg: impl fmt::Display) {
if !self.enabled {
return;
}

if self.ignore_trivia && self.parsers.last().map_or(false, |p| p.contains("trivia")) {
return;
}

eprintln!(
"{prefix}{indent}{msg}",
prefix = self.log_prefix(preview),
indent = " ".repeat(self.depth() * 2)
);
}

#[inline]
fn depth(&self) -> usize {
self.parsers.len()
}

#[inline]
fn log_prefix(&self, preview: SourcePreview) -> impl fmt::Display {
LogPrefix {
preview,
depth: self.depth(),
with_color: self.color == AnsiColored::Yes,
}
}
}

struct LogPrefix {
preview: SourcePreview,
depth: usize,
with_color: bool,
}

impl fmt::Display for LogPrefix {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "|")?;
if self.with_color {
write!(f, "\x1b[2m")?;
}
write!(f, "{:░<15}", self.preview)?;
if self.with_color {
write!(f, "\x1b[0m")?;
}
write!(f, "|")?;
write!(f, "[{depth:02}]", depth = self.depth)?;

Ok(())
}
}

pub(crate) struct SourcePreview(String);

impl fmt::Display for SourcePreview {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

impl<'a> ParserContext<'a> {
pub(crate) fn preview(&self) -> SourcePreview {
let preview = self.source()[self.position().utf8..]
.chars()
.take(PREVIEW_LEN)
.collect::<String>();

SourcePreview(
preview
.replace('\n', "␊")
.replace('\r', "␍")
.replace('\t', "␉")
.replace('\0', "␀"),
)
}
}

pub struct EnterParseGuard<'a, 'b> {
context: &'a mut ParserContext<'b>,
}

impl<'a, 'b> EnterParseGuard<'a, 'b> {
pub fn context(&mut self) -> &mut ParserContext<'b> {
self.context
}
}

impl<'a, 'b> Drop for EnterParseGuard<'a, 'b> {
fn drop(&mut self) {
let parser_name = DEBUGGER.write().unwrap().parsers.pop().unwrap();

if let Ok(debugger) = DEBUGGER.read() {
if !debugger.ignore_trivia || !parser_name.contains("trivia") {
let msg = format!(
"{start}{parser_name} (exit){end}",
start = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[1m")
.unwrap_or_default(),
end = (debugger.color == AnsiColored::Yes)
.then_some("\x1b[0m")
.unwrap_or_default()
);

debugger.log_by_ref(SourcePreview(String::new()), msg);
}
}
}
}
1 change: 1 addition & 0 deletions crates/codegen/parser/runtime/src/parser_support/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod choice_helper;
mod context;
pub(crate) mod debugger;
mod optional_helper;
mod parser_function;
mod parser_result;
Expand Down
10 changes: 9 additions & 1 deletion crates/codegen/parser/runtime/src/templates/language.rs.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::lexer::{KeywordScan, Lexer, ScannedToken};
#[cfg(feature = "slang_napi_interfaces")]
use crate::napi_interface::parse_output::ParseOutput as NAPIParseOutput;
use crate::parse_output::ParseOutput;
use crate::parser_support::debugger::Debugger;
use crate::parser_support::{
ChoiceHelper, OneOrMoreHelper, OptionalHelper, ParserContext, ParserFunction, ParserResult,
PrecedenceHelper, RecoverFromNoMatch, SeparatedHelper, SequenceHelper, ZeroOrMoreHelper,
Expand Down Expand Up @@ -80,7 +81,12 @@ impl Language {

{% for parser_name, parser_code in generator.parser_functions %}
#[allow(unused_assignments, unused_parens)]
fn {{ parser_name | snake_case }}(&self, input: &mut ParserContext<'_>) -> ParserResult { {{ parser_code }} }
fn {{ parser_name | snake_case }}(&self, input: &mut ParserContext<'_>) -> ParserResult {
let mut guard = Debugger::enter_parser(input, "{{ parser_name | snake_case }}");
let input = guard.context();

{{ parser_code }}
}
{% endfor %}

/********************************************
Expand All @@ -99,6 +105,8 @@ impl Language {
{% endfor %}

pub fn parse(&self, kind: RuleKind, input: &str) -> ParseOutput {
Debugger::start_parse(kind, self.version.to_string());

match kind {
{%- for parser_name, _ in generator.parser_functions -%}
{# TODO(#737): Remove the special case once we stop generating RuleKind for trivia #}
Expand Down
1 change: 1 addition & 0 deletions crates/solidity/outputs/cargo/crate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ anyhow = { workspace = true, optional = true }
ariadne = { workspace = true }
clap = { workspace = true, optional = true }
nom = { workspace = true }
once_cell = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true, optional = true }
Expand Down
Loading

0 comments on commit 22d2513

Please sign in to comment.