diff --git a/crates/plotnik-lib/src/bytecode/constants.rs b/crates/plotnik-lib/src/bytecode/constants.rs index acf2f1ce..0bd5f1f0 100644 --- a/crates/plotnik-lib/src/bytecode/constants.rs +++ b/crates/plotnik-lib/src/bytecode/constants.rs @@ -18,3 +18,10 @@ pub const STEP_SIZE: usize = 8; /// instead of comparing type IDs. This distinguishes `(_)` (any named) /// from `_` (any node including anonymous). pub const NAMED_WILDCARD: u16 = 0xFFFF; + +/// Maximum payload slots for Match instructions. +/// +/// Match64 (the largest variant) supports up to 28 u16 slots for +/// effects, neg_fields, and successors combined. When an epsilon +/// transition needs more successors, it must be split into a cascade. +pub const MAX_MATCH_PAYLOAD_SLOTS: usize = 28; diff --git a/crates/plotnik-lib/src/bytecode/mod.rs b/crates/plotnik-lib/src/bytecode/mod.rs index 7579d62d..483fe76b 100644 --- a/crates/plotnik-lib/src/bytecode/mod.rs +++ b/crates/plotnik-lib/src/bytecode/mod.rs @@ -16,7 +16,9 @@ mod nav; mod sections; mod type_meta; -pub use constants::{MAGIC, NAMED_WILDCARD, SECTION_ALIGN, STEP_SIZE, VERSION}; +pub use constants::{ + MAGIC, MAX_MATCH_PAYLOAD_SLOTS, NAMED_WILDCARD, SECTION_ALIGN, STEP_SIZE, VERSION, +}; pub use ids::{QTypeId, StringId}; diff --git a/crates/plotnik-lib/src/compile/mod.rs b/crates/plotnik-lib/src/compile/mod.rs index b1733f3c..1cb8497a 100644 --- a/crates/plotnik-lib/src/compile/mod.rs +++ b/crates/plotnik-lib/src/compile/mod.rs @@ -401,4 +401,33 @@ mod tests { assert!(!result.instructions.is_empty()); } + + #[test] + fn compile_large_tagged_alternation() { + // Regression test: alternations with 30+ branches should compile + // by splitting epsilon transitions into a cascade. + let branches: String = (0..30) + .map(|i| format!("A{i}: (identifier) @x{i}")) + .collect::>() + .join(" "); + let query_str = format!("Q = [{branches}]"); + + let query = QueryBuilder::one_liner(&query_str) + .parse() + .unwrap() + .analyze(); + + let mut strings = StringTableBuilder::new(); + let result = Compiler::compile( + query.interner(), + query.type_context(), + &query.symbol_table, + &mut strings, + None, + None, + ) + .unwrap(); + + assert!(!result.instructions.is_empty()); + } } diff --git a/crates/plotnik-lib/src/compile/scope.rs b/crates/plotnik-lib/src/compile/scope.rs index 9795c3d4..e6dfafbc 100644 --- a/crates/plotnik-lib/src/compile/scope.rs +++ b/crates/plotnik-lib/src/compile/scope.rs @@ -484,17 +484,46 @@ impl Compiler<'_> { } /// Emit an epsilon transition (no node interaction). + /// + /// If there are more successors than fit in a single Match instruction, + /// this creates a cascade of epsilon transitions to preserve NFA semantics. pub(super) fn emit_epsilon(&mut self, label: Label, successors: Vec