From d32310a5a3a5a68e0bf0fd43c9787e8b0ccdcba0 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Wed, 3 Dec 2025 11:42:04 -0300 Subject: [PATCH] feat: Use `debug_fuel` in release mode --- crates/plotnik-lib/src/ast/parser/core.rs | 45 +++++-------------- .../plotnik-lib/src/ast/parser/invariants.rs | 14 +++--- .../src/ast/parser/tests/recovery/coverage.rs | 6 +-- crates/plotnik-lib/src/query/mod.rs | 19 -------- 4 files changed, 20 insertions(+), 64 deletions(-) diff --git a/crates/plotnik-lib/src/ast/parser/core.rs b/crates/plotnik-lib/src/ast/parser/core.rs index b3b57a86..d490749e 100644 --- a/crates/plotnik-lib/src/ast/parser/core.rs +++ b/crates/plotnik-lib/src/ast/parser/core.rs @@ -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; @@ -63,11 +60,8 @@ pub struct Parser<'src> { pub(super) delimiter_stack: Vec, // 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, - #[cfg(debug_assertions)] - pub(super) debug_fuel_limit: Option, /// Execution fuel. Never replenishes. exec_fuel_remaining: Option, @@ -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) -> 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) -> Self { self.exec_fuel_remaining = limit; @@ -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) @@ -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(); @@ -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(); @@ -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. diff --git a/crates/plotnik-lib/src/ast/parser/invariants.rs b/crates/plotnik-lib/src/ast/parser/invariants.rs index cd95d2f0..a51dadd4 100644 --- a/crates/plotnik-lib/src/ast/parser/invariants.rs +++ b/crates/plotnik-lib/src/ast/parser/invariants.rs @@ -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] diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs index 603e57e0..5a73a527 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs @@ -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)), @@ -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)), @@ -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)), diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 731841ca..ac55d9b8 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -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>, exec_fuel: Option>, recursion_fuel: Option>, } @@ -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) -> Self { - self.debug_fuel = Some(limit); - self - } - /// Set execution fuel limit. None = infinite. /// /// Execution fuel never replenishes. It protects against large inputs. @@ -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); }