Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 11 additions & 34 deletions crates/plotnik-lib/src/ast/parser/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ use crate::ast::lexer::{Token, token_text};
use crate::ast::syntax_kind::token_sets::ROOT_EXPR_FIRST;
use crate::ast::syntax_kind::{SyntaxKind, TokenSet};

#[cfg(debug_assertions)]
const DEFAULT_DEBUG_FUEL: u32 = 256;

const DEFAULT_EXEC_FUEL: u32 = 1_000_000;
const DEFAULT_RECURSION_FUEL: u32 = 512;

Expand Down Expand Up @@ -63,11 +60,8 @@ pub struct Parser<'src> {
pub(super) delimiter_stack: Vec<OpenDelimiter>,

// Fuel limits
/// Debug-only: loop detection fuel. Resets on bump(). Panics when exhausted.
#[cfg(debug_assertions)]
/// Loop detection fuel. Resets on bump(). Panics when exhausted.
pub(super) debug_fuel: std::cell::Cell<u32>,
#[cfg(debug_assertions)]
pub(super) debug_fuel_limit: Option<u32>,

/// Execution fuel. Never replenishes.
exec_fuel_remaining: Option<u32>,
Expand All @@ -91,24 +85,13 @@ impl<'src> Parser<'src> {
depth: 0,
last_error_pos: None,
delimiter_stack: Vec::with_capacity(8),
#[cfg(debug_assertions)]
debug_fuel: std::cell::Cell::new(DEFAULT_DEBUG_FUEL),
#[cfg(debug_assertions)]
debug_fuel_limit: Some(DEFAULT_DEBUG_FUEL),
debug_fuel: std::cell::Cell::new(256),
exec_fuel_remaining: Some(DEFAULT_EXEC_FUEL),
recursion_fuel_limit: Some(DEFAULT_RECURSION_FUEL),
fatal_error: None,
}
}

/// Set debug fuel limit (debug builds only). None = infinite.
#[cfg(debug_assertions)]
pub fn with_debug_fuel(mut self, limit: Option<u32>) -> Self {
self.debug_fuel_limit = limit;
self.debug_fuel.set(limit.unwrap_or(u32::MAX));
self
}

/// Set execution fuel limit. None = infinite.
pub fn with_exec_fuel(mut self, limit: Option<u32>) -> Self {
self.exec_fuel_remaining = limit;
Expand Down Expand Up @@ -142,15 +125,13 @@ impl<'src> Parser<'src> {
self.nth(0)
}

fn reset_debug_fuel(&self) {
self.debug_fuel.set(256);
}

/// Lookahead by `n` tokens (0 = current). Consumes debug fuel (panics if stuck).
pub(super) fn nth(&self, lookahead: usize) -> SyntaxKind {
#[cfg(debug_assertions)]
{
self.assert_progress();
if self.debug_fuel_limit.is_some() {
self.debug_fuel.set(self.debug_fuel.get() - 1);
}
}
self.ensure_progress();

self.tokens
.get(self.pos + lookahead)
Expand Down Expand Up @@ -266,10 +247,7 @@ impl<'src> Parser<'src> {
pub(super) fn bump(&mut self) {
assert!(!self.eof(), "bump called at EOF");

#[cfg(debug_assertions)]
if let Some(limit) = self.debug_fuel_limit {
self.debug_fuel.set(limit);
}
self.reset_debug_fuel();

self.consume_exec_fuel();

Expand All @@ -283,10 +261,7 @@ impl<'src> Parser<'src> {
pub(super) fn skip_token(&mut self) {
assert!(!self.eof(), "skip_token called at EOF");

#[cfg(debug_assertions)]
if let Some(limit) = self.debug_fuel_limit {
self.debug_fuel.set(limit);
}
self.reset_debug_fuel();

self.consume_exec_fuel();

Expand Down Expand Up @@ -398,11 +373,13 @@ impl<'src> Parser<'src> {
return false;
}
self.depth += 1;
self.reset_debug_fuel();
true
}

pub(super) fn exit_recursion(&mut self) {
self.depth = self.depth.saturating_sub(1);
self.reset_debug_fuel();
}

/// Push an opening delimiter onto the stack for tracking unclosed constructs.
Expand Down
14 changes: 6 additions & 8 deletions crates/plotnik-lib/src/ast/parser/invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ use crate::ast::syntax_kind::SyntaxKind;

impl Parser<'_> {
#[inline]
#[cfg(debug_assertions)]
pub(super) fn assert_progress(&self) {
if let Some(limit) = self.debug_fuel_limit {
assert!(
self.debug_fuel.get() != 0,
"parser is stuck: no progress made in {limit} iterations"
);
}
pub(super) fn ensure_progress(&self) {
assert!(
self.debug_fuel.get() != 0,
"parser is stuck: too many lookaheads"
);
self.debug_fuel.set(self.debug_fuel.get() - 1);
}

#[inline]
Expand Down
6 changes: 3 additions & 3 deletions crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ fn deeply_nested_trees_hit_recursion_limit() {
input.push(')');
}

let result = Query::builder(&input).with_debug_fuel(None).build();
let result = Query::builder(&input).build();

assert!(
matches!(result, Err(crate::Error::RecursionLimitExceeded)),
Expand All @@ -351,7 +351,7 @@ fn deeply_nested_sequences_hit_recursion_limit() {
input.push('}');
}

let result = Query::builder(&input).with_debug_fuel(None).build();
let result = Query::builder(&input).build();

assert!(
matches!(result, Err(crate::Error::RecursionLimitExceeded)),
Expand All @@ -372,7 +372,7 @@ fn deeply_nested_alternations_hit_recursion_limit() {
input.push(']');
}

let result = Query::builder(&input).with_debug_fuel(None).build();
let result = Query::builder(&input).build();

assert!(
matches!(result, Err(crate::Error::RecursionLimitExceeded)),
Expand Down
19 changes: 0 additions & 19 deletions crates/plotnik-lib/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ use shape_cardinalities::ShapeCardinality;
/// Builder for configuring and creating a [`Query`].
pub struct QueryBuilder<'a> {
source: &'a str,
#[cfg(debug_assertions)]
debug_fuel: Option<Option<u32>>,
exec_fuel: Option<Option<u32>>,
recursion_fuel: Option<Option<u32>>,
}
Expand All @@ -37,23 +35,11 @@ impl<'a> QueryBuilder<'a> {
pub fn new(source: &'a str) -> Self {
Self {
source,
#[cfg(debug_assertions)]
debug_fuel: None,
exec_fuel: None,
recursion_fuel: None,
}
}

/// Set debug fuel limit (debug builds only). None = infinite.
///
/// Debug fuel resets on each token consumed. It detects parser bugs
/// where no progress is made. Panics when exhausted.
#[cfg(debug_assertions)]
pub fn with_debug_fuel(mut self, limit: Option<u32>) -> Self {
self.debug_fuel = Some(limit);
self
}

/// Set execution fuel limit. None = infinite.
///
/// Execution fuel never replenishes. It protects against large inputs.
Expand All @@ -79,11 +65,6 @@ impl<'a> QueryBuilder<'a> {
let tokens = lex(self.source);
let mut parser = Parser::new(self.source, tokens);

#[cfg(debug_assertions)]
if let Some(limit) = self.debug_fuel {
parser = parser.with_debug_fuel(limit);
}

if let Some(limit) = self.exec_fuel {
parser = parser.with_exec_fuel(limit);
}
Expand Down