From cb48e8b40703b955391070642e555f82771a910c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Wed, 3 Dec 2025 12:13:13 -0300 Subject: [PATCH] fix: Recursion and exec fuel interference with `debug_fuel` --- crates/plotnik-lib/src/ast/parser/core.rs | 2 +- .../src/ast/parser/tests/recovery/coverage.rs | 88 ++++++++++++++++--- .../ast/parser/tests/recovery/incomplete.rs | 20 ----- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/crates/plotnik-lib/src/ast/parser/core.rs b/crates/plotnik-lib/src/ast/parser/core.rs index b3b57a86..141d6d8a 100644 --- a/crates/plotnik-lib/src/ast/parser/core.rs +++ b/crates/plotnik-lib/src/ast/parser/core.rs @@ -20,7 +20,7 @@ use crate::ast::syntax_kind::{SyntaxKind, TokenSet}; const DEFAULT_DEBUG_FUEL: u32 = 256; const DEFAULT_EXEC_FUEL: u32 = 1_000_000; -const DEFAULT_RECURSION_FUEL: u32 = 512; +const DEFAULT_RECURSION_FUEL: u32 = 4096; /// Parse result containing the green tree and any errors. /// 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..c4ba10ca 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/coverage.rs @@ -320,17 +320,18 @@ fn capture_at_start_of_alternation() { #[test] fn deeply_nested_trees_hit_recursion_limit() { - // Test just over recursion limit (default is 512) - let depth = 513; + let depth = 128; let mut input = String::new(); - for _ in 0..depth { + for _ in 0..depth + 1 { input.push_str("(a "); } for _ in 0..depth { input.push(')'); } - let result = Query::builder(&input).with_debug_fuel(None).build(); + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -341,17 +342,18 @@ fn deeply_nested_trees_hit_recursion_limit() { #[test] fn deeply_nested_sequences_hit_recursion_limit() { - // Test just over recursion limit (default is 512) - let depth = 513; + let depth = 128; let mut input = String::new(); - for _ in 0..depth { + for _ in 0..depth + 1 { input.push_str("{(a) "); } for _ in 0..depth { input.push('}'); } - let result = Query::builder(&input).with_debug_fuel(None).build(); + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -362,17 +364,18 @@ fn deeply_nested_sequences_hit_recursion_limit() { #[test] fn deeply_nested_alternations_hit_recursion_limit() { - // Test just over recursion limit (default is 512) - let depth = 513; + let depth = 128; let mut input = String::new(); - for _ in 0..depth { + for _ in 0..depth + 1 { input.push_str("[(a) "); } for _ in 0..depth { input.push(']'); } - let result = Query::builder(&input).with_debug_fuel(None).build(); + let result = Query::builder(&input) + .with_recursion_fuel(Some(depth)) + .build(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -964,3 +967,64 @@ fn paren_close_inside_sequence() { | ^^^^ unnamed definition must be last in file; add a name: `Name = {(a)` "#); } + +#[test] +fn many_trees_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + for _ in 0..count { + input.push_str("(a) "); + } + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} + +#[test] +fn many_branches_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + input.push('['); + for i in 0..count { + if i > 0 { + input.push(' '); + } + input.push_str("(a)"); + } + input.push(']'); + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} + +#[test] +fn many_fields_exhaust_exec_fuel() { + let count = 500; + let mut input = String::new(); + input.push('('); + for i in 0..count { + if i > 0 { + input.push(' '); + } + input.push_str("a: (b)"); + } + input.push(')'); + + let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + + assert!( + matches!(result, Err(crate::Error::ExecFuelExhausted)), + "expected ExecFuelExhausted error, got {:?}", + result + ); +} diff --git a/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs b/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs index 383790da..7561167c 100644 --- a/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs +++ b/crates/plotnik-lib/src/ast/parser/tests/recovery/incomplete.rs @@ -208,23 +208,3 @@ fn bare_missing_keyword() { | ^^^^^^^ ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) "#); } - -#[test] -fn deep_nesting_within_limit() { - let depth = 100; - let mut input = String::new(); - for _ in 0..depth { - input.push_str("(a "); - } - for _ in 0..depth { - input.push(')'); - } - - let result = crate::ast::parser::parse(&input).unwrap(); - assert!( - result.is_valid(), - "expected no errors for depth {}, got: {:?}", - depth, - result.errors() - ); -}