Repository navigation
Non-consuming multi-context set grows the context stack without making progress #712
Description
Summary
ParseState::parse_line does not return within a 2-second deadline when a zero-width match repeatedly executes set: [main, main]. Each transition replaces one context with two at the same input position, so the context stack keeps growing.
Minimal reproducer
Create a Cargo project with these two files.
Cargo.toml:
[package]
name = "syntect-set-repro"
version = "0.1.0"
edition = "2021"
[dependencies]
syntect = { version = "=5.3.0", default-features = false }
[features]
default = ["onig"]
onig = ["syntect/default-onig"]
fancy = ["syntect/default-fancy"]src/main.rs:
use syntect::parsing::{ParseState, SyntaxDefinition, SyntaxSetBuilder};
fn main() {
let syntax = r#"
name: Synthetic
scope: source.synthetic
contexts:
main:
- match: '(?=x)'
set: [main, main]
"#;
let definition = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
let mut builder = SyntaxSetBuilder::new();
builder.add(definition);
let syntaxes = builder.build();
let mut parser = ParseState::new(&syntaxes.syntaxes()[0]);
let input = std::env::args().nth(1).unwrap_or_else(|| "x".to_owned());
eprintln!("Starting parse_line({input:?})");
let result = parser.parse_line(&input, &syntaxes);
eprintln!("parse_line returned: {result:?}");
}Build first, then run with a deadline so compilation time is excluded:
cargo build --release
python3 - <<'PY'
import subprocess
for value in ("y", "x"):
print(f"Testing input {value!r}", flush=True)
try:
subprocess.run(
["target/release/syntect-set-repro", value],
check=True,
timeout=2,
)
except subprocess.TimeoutExpired:
print("Timed out after 2 seconds; child killed and reaped.", flush=True)
PYThe Python timeout kills and reaps the child process. To test the other regex engine, rebuild with the following command and run the same Python snippet:
cargo build --release --no-default-features --features fancyExpected and actual behavior
Expected: parsing terminates with a result or a bounded error, even if this cyclic grammar cannot produce useful highlighting.
Actual: y prints parse_line returned: Ok(...) and exits successfully. For x, the last parser message is Starting parse_line("x"); it never prints a return value before the 2-second deadline kills the process. The timeout is an observation window, not a measurement of eventual completion.
Versions tested
Verified on 2026-10-21 using release builds on macOS/aarch64, with Rust 1.99.0:
| Syntect | Oniguruma | fancy-regex |
|---|---|---|
| crates.io 5.3.0 | x: timed out; y: returned |
x: timed out; y: returned |
master 4aa78031e93ebd3e0be7278120d0bd9d2508b1a3 |
x: timed out; y: returned |
x: timed out; y: returned |
Resolved regex versions: Oniguruma wrapper onig 6.5.3 for both; fancy-regex 0.16.2 for 5.3.0 and 0.18.0 for master.
Possible cause and related work
In src/parsing/parser.rs, push_too_deep covers Push (and Branch/Embed on master), but does not cover Set. Here, (?=x) consumes no input and each set increases the stack depth by one, bypassing that depth guard.
This differs from the flat-stack set cycle described in #706: the full stack changes on every iteration. #708 proposes optional parsing budgets; this reproduction uses the existing parse_line API. Both PRs were open when checked on 2026-10-21.
Activity