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
2 changes: 1 addition & 1 deletion crates/plotnik-lib/src/compile/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Compiler<'_> {
true,
None,
CaptureEffects::default(),
None, // No skip_exit bypass - all paths need Up
Some(final_exit), // Skip exit bypasses Up when Down fails (childless node)
);

self.instructions
Expand Down
21 changes: 17 additions & 4 deletions crates/plotnik-lib/src/compile/quantifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,23 @@ impl Compiler<'_> {
self.emit_null_for_internal_captures(null_exit, inner)
};

// Skip-retry iteration leading to null exit
// Skip-retry iteration:
// For Optional, both skip paths (entry-level and retry-exhaust) need null.
// - Entry-level (Down fails): skip_with_null (bypass Up, has null from caller)
// - Retry exhaust (Down succeeded but pattern fails): need Up, then null
// When needs_split_exits, create a retry exhaust path with null → match_exit.
let first_nav_mode = first_nav.unwrap_or(Nav::Down);
let iterate_exit = if needs_split_exits {
// Retry exhaust needs null injection then goes to match_exit (for Up)
let null_exit = self.emit_null_for_skip_path(match_exit, &element_capture);
self.emit_null_for_internal_captures(null_exit, inner)
} else {
skip_with_null
};
let iterate = self.compile_skip_retry_iteration(
first_nav_mode,
body_entry,
skip_with_null,
iterate_exit,
is_greedy,
);

Expand Down Expand Up @@ -554,10 +565,12 @@ impl Compiler<'_> {
self.compile_expr_inner(inner, loop_entry, Some(Nav::StayExact), capture)
};

// First iteration: skip-retry with skip_exit as fallback
// First iteration: skip-retry with match_exit as internal fallback.
// When retry exhausts (Down succeeded but pattern fails), we need Up via match_exit.
// The entry-level epsilon (below) handles Down failure → skip_exit (bypass Up).
let first_nav_mode = nav_override.unwrap_or(Nav::Down);
let first_iterate =
self.compile_skip_retry_iteration(first_nav_mode, body_entry, skip_exit, is_greedy);
self.compile_skip_retry_iteration(first_nav_mode, body_entry, match_exit, is_greedy);

// Repeat iteration: skip-retry with match_exit as fallback
let repeat_iterate =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,23 @@ Test:
06 ε 07
07 ! (object) 08
08 ε [Arr] 10
10 ε 37, 12
10 ε 39, 20
12 ε [EndArr Set(M5)] 14
14 △ _ 19
15 ε [EndObj Push] 17
17 ε 43, 12
17 ε 45, 12
19 ▶
20 ε [Set(M4)] 15
22 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 20
25 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 20
28 ε 22, 25
30 ε [Obj] 28
32 ▷ _ 35
33 ε 32, 12
35 ε 30, 33
37 ▽ _ 35
38 ▷ _ 41
39 ε 38, 12
41 ε 30, 39
43 ▷ _ 41
20 ε [EndArr Set(M5)] 19
22 ε [Set(M4)] 15
24 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 22
27 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 22
30 ε 24, 27
32 ε [Obj] 30
34 ▷ _ 37
35 ε 34, 12
37 ε 32, 35
39 ▽ _ 37
40 ▷ _ 43
41 ε 40, 12
43 ε 32, 41
45 ▷ _ 43
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,23 @@ Outer:
12 ε 13
13 ! (parent) 14
14 ε [Arr] 16
16 ε 40, 18
16 ε 42, 26
18 ε [EndArr Set(M2)] 20
20 △ _ 25
21 ε [EndObj Push] 23
23 ε 46, 18
23 ε 48, 18
25 ▶
26 ε [Set(M1)] 21
28 ε [EndObj] 26
30 ! (Inner) 06 : 28
31 ε [Obj] 30
33 ε [Obj] 31
35 ▷ _ 38
36 ε 35, 18
38 ε 33, 36
40 ▽ _ 38
41 ▷ _ 44
42 ε 41, 18
44 ε 33, 42
46 ▷ _ 44
26 ε [EndArr Set(M2)] 25
28 ε [Set(M1)] 21
30 ε [EndObj] 28
32 ! (Inner) 06 : 30
33 ε [Obj] 32
35 ε [Obj] 33
37 ▷ _ 40
38 ε 37, 18
40 ε 35, 38
42 ▽ _ 40
43 ▷ _ 46
44 ε 43, 18
46 ε 35, 44
48 ▷ _ 46
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ _ObjWrap:
Test:
06 ε 07
07 ! (program) 08
08 ε 24, 15
08 ε 26, 15
10 ▶
11 ▽ (number) [Node Set(M1)] 25
13 ▷ (number) [Node Set(M1)] 25
11 ▽ (number) [Node Set(M1)] 27
13 ▷ (number) [Node Set(M1)] 27
15 ε [Null Set(M0)] 11
17 ! (identifier) [Node Set(M0)] 13
19 ▷ _ 22
20 ε 19, 15
22 ε 17, 20
24 ▽ _ 22
25 △ _ 10
19 ε [Null Set(M0)] 13
21 ▷ _ 24
22 ε 21, 19
24 ε 17, 22
26 ▽ _ 24
27 △ _ 10
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ _ObjWrap:
Test:
06 ε 07
07 ! (function_declaration) 08
08 ε 20, 13
08 ε 22, 11
10 ▶
11 ! (decorator) [Node Set(M0)] 21
13 ε [Null Set(M0)] 21
15 ▷ _ 18
16 ε 15, 13
18 ε 11, 16
20 ▽ _ 18
21 △ _ 10
11 ε [Null Set(M0)] 10
13 ! (decorator) [Node Set(M0)] 23
15 ε [Null Set(M0)] 23
17 ▷ _ 20
18 ε 17, 15
20 ε 13, 18
22 ▽ _ 20
23 △ _ 10
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Test:
21 ε [EndArr Set(M0)] 19
23 ε [EndArr Set(M0)] 17
25 ▷ _ 28
26 ε 25, 23
26 ε 25, 21
28 ε 12, 26
30 ▽ _ 28
31 ▷ _ 34
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ _ObjWrap:
Test:
06 ε 07
07 ! (function_declaration) 08
08 ε 20, 13
08 ε 22, 11
10 ▶
11 ! (decorator) [Node Set(M0)] 21
13 ε [Null Set(M0)] 21
15 ▷ _ 18
16 ε 15, 13
18 ε 11, 16
20 ▽ _ 18
21 △ _ 10
11 ε [Null Set(M0)] 10
13 ! (decorator) [Node Set(M0)] 23
15 ε [Null Set(M0)] 23
17 ▷ _ 20
18 ε 17, 15
20 ε 13, 18
22 ▽ _ 20
23 △ _ 10
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ _ObjWrap:
Test:
06 ε 07
07 ! (function_declaration) 08
08 ε 13, 20
08 ε 11, 22
10 ▶
11 ! (decorator) [Node Set(M0)] 21
13 ε [Null Set(M0)] 21
15 ▷ _ 18
16 ε 13, 15
18 ε 16, 11
20 ▽ _ 18
21 △ _ 10
11 ε [Null Set(M0)] 10
13 ! (decorator) [Node Set(M0)] 23
15 ε [Null Set(M0)] 23
17 ▷ _ 20
18 ε 15, 17
20 ε 18, 13
22 ▽ _ 20
23 △ _ 10
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ Test:
06 ε 07
07 ! (function_declaration) 08
08 ε [Arr] 10
10 ε 25, 12
10 ε 27, 20
12 ε [EndArr Set(M0)] 14
14 △ _ 19
15 ! (decorator) [Node Push] 17
17 ε 31, 12
17 ε 33, 12
19 ▶
20 ▷ _ 23
21 ε 20, 12
23 ε 15, 21
25 ▽ _ 23
26 ▷ _ 29
27 ε 26, 12
29 ε 15, 27
31 ▷ _ 29
20 ε [EndArr Set(M0)] 19
22 ▷ _ 25
23 ε 22, 12
25 ε 15, 23
27 ▽ _ 25
28 ▷ _ 31
29 ε 28, 12
31 ε 15, 29
33 ▷ _ 31
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,21 @@ Test:
06 ε 07
07 ! (array) 08
08 ε [Arr] 10
10 ε 31, 12
10 ε 33, 20
12 ε [EndArr Set(M2)] 14
14 △ _ 19
15 ε [EndObj Push] 17
17 ε 37, 12
17 ε 39, 12
19 ▶
20 ▷ (number) [Node Set(M1)] 15
22 ! (identifier) [Node Set(M0)] 20
24 ε [Obj] 22
26 ▷ _ 29
27 ε 26, 12
29 ε 24, 27
31 ▽ _ 29
32 ▷ _ 35
33 ε 32, 12
35 ε 24, 33
37 ▷ _ 35
20 ε [EndArr Set(M2)] 19
22 ▷ (number) [Node Set(M1)] 15
24 ! (identifier) [Node Set(M0)] 22
26 ε [Obj] 24
28 ▷ _ 31
29 ε 28, 12
31 ε 26, 29
33 ▽ _ 31
34 ▷ _ 37
35 ε 34, 12
37 ε 26, 35
39 ▷ _ 37
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,19 @@ Test:
06 ε 07
07 ! (parent) 08
08 ε [Arr] 10
10 ε 26, 12
10 ε 28, 20
12 ε [EndArr Set(M0)] 14
14 △ _ 19
15 ▷ (b) [Node Push] 17
17 ε 32, 12
17 ε 34, 12
19 ▶
20 ! (a) 15
21 ▷ _ 24
22 ε 21, 12
24 ε 20, 22
26 ▽ _ 24
27 ▷ _ 30
28 ε 27, 12
30 ε 20, 28
32 ▷ _ 30
20 ε [EndArr Set(M0)] 19
22 ! (a) 15
23 ▷ _ 26
24 ε 23, 12
26 ε 22, 24
28 ▽ _ 26
29 ▷ _ 32
30 ε 29, 12
32 ε 22, 30
34 ▷ _ 32
38 changes: 38 additions & 0 deletions crates/plotnik-lib/src/engine/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,41 @@ fn regression_nested_quantifiers_struct_captures_mixed() {
"class Empty { } class One { foo() {} } class Two { bar() {} baz() {} }"
);
}

/// BUG #9: Childless nodes with inner quantifiers caused outer quantifier to miss matches.
///
/// For `(identifier (_)* @items)` inside a star quantifier, if `identifier` has zero
/// children at runtime (tree-sitter identifiers are terminal nodes with no children),
/// the Down navigation fails. The skip path should NOT execute Up since we never
/// descended, but currently it does, causing the cursor to ascend one level too high.
///
/// This breaks the outer quantifier's iteration: after processing the first match,
/// the cursor is at the grandparent level instead of sibling level, so Next fails.
///
/// Fix: When Down fails immediately (childless node), the skip path should bypass
/// the Up instruction. Only execute Up when we actually descended into children.
#[test]
fn regression_childless_node_with_inner_star() {
snap!(
indoc! {r#"
Q = (program {(expression_statement
(identifier (_)* @items) @id
) @stmt}* @stmts)
"#},
"foo; bar; baz"
);
}

/// Same bug with optional quantifier instead of star.
/// `(identifier (_)? @item)` should also handle childless nodes correctly.
#[test]
fn regression_childless_node_with_inner_optional() {
snap!(
indoc! {r#"
Q = (program {(expression_statement
(identifier (_)? @item) @id
) @stmt}* @stmts)
"#},
"foo; bar; baz"
);
}
Loading