mirror of
https://github.com/tree-sitter/tree-sitter.git
synced 2026-09-10 07:36:22 -04:00
fix(query): short-circuit longest-match dedup for disjoint states
An unanchored quantified sibling like `(program (comment)+ @doc (class))`
keeps one match state per matching sibling. A recent fix stopped over-pruning
them, but the per-node longest-match dedup is pairwise, so with n live states
matching became O(n^3).
Two states can only be capture subsets of one another if their captured
byte ranges overlap, so keep each group ordered by first-capture position
and stop the pairwise scan once the rest of the group is disjoint.
Refs neovim/neovim#40517
(cherry picked from commit 91fd04f96a)
This commit is contained in:
parent
074b6eb05b
commit
e7d6651336
|
|
@ -1348,6 +1348,35 @@ fn test_query_matches_with_repeated_leaf_nodes() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_optional_capture_before_uncaptured_required_sibling() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("rust");
|
||||
let query = Query::new(&language, "(block (line_comment)? @doc (line_comment))").unwrap();
|
||||
|
||||
// The optional `(line_comment)? @doc` before an *uncaptured* required
|
||||
// `(line_comment)` yields two candidate completions at the block:
|
||||
// - one where the optional captured `// a` (the required node is then `// b`),
|
||||
// - one where the optional matched zero (the required node absorbs `// a`,
|
||||
// leaving `@doc` unbound).
|
||||
// The zero-match completion's captures are a strict subset of the other's, so the
|
||||
// longest-match rule must drop it (there is exactly one match). This regressed when
|
||||
// the dedup pass gained an early-break. Without the accompanying capture-position
|
||||
// sort, the break skips the subset "loser" and it leaks as an extra empty match.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
fn f() {
|
||||
// a
|
||||
// b
|
||||
}
|
||||
",
|
||||
&[(0, vec![("doc", "// a")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_optional_nodes_inside_of_repetitions() {
|
||||
allocations::record(|| {
|
||||
|
|
|
|||
|
|
@ -3654,6 +3654,46 @@ void ts_query_cursor__compare_captures(
|
|||
}
|
||||
}
|
||||
|
||||
// Order two in-progress states for the longest-match dedup pass. Within a
|
||||
// (start_depth, pattern_index) group, states with no captures sort first, as they are a
|
||||
// subset of every other state (so the dedup pass must always compare them). The rest
|
||||
// sort by the start byte of their first capture.
|
||||
static bool ts_query_cursor__state_precedes(
|
||||
const TSQueryCursor *self,
|
||||
const QueryState *a,
|
||||
const QueryState *b
|
||||
) {
|
||||
if (a->start_depth != b->start_depth) return a->start_depth < b->start_depth;
|
||||
if (a->pattern_index != b->pattern_index) return a->pattern_index < b->pattern_index;
|
||||
const CaptureList *a_caps = capture_list_pool_get(&self->capture_list_pool, a->capture_list_id);
|
||||
const CaptureList *b_caps = capture_list_pool_get(&self->capture_list_pool, b->capture_list_id);
|
||||
if ((a_caps->size == 0) != (b_caps->size == 0)) return a_caps->size == 0;
|
||||
if (a_caps->size == 0) return false;
|
||||
return
|
||||
ts_node_start_byte(array_get(a_caps, 0)->node) <
|
||||
ts_node_start_byte(array_get(b_caps, 0)->node);
|
||||
}
|
||||
|
||||
// Stable-sort the in-progress states with the order dictated by `ts_query_cursor__state_precedes`.
|
||||
// This runs once per node, right before the dedup pass.
|
||||
static void ts_query_cursor__sort_states_by_capture(TSQueryCursor *self) {
|
||||
QueryStateList *states = &self->states;
|
||||
for (uint32_t i = 1; i < states->size; i++) {
|
||||
// Fast+common path: this state is already ordered after its predecessor, so it does not need
|
||||
// to move.
|
||||
if (!ts_query_cursor__state_precedes(
|
||||
self, array_get(states, i), array_get(states, i - 1)
|
||||
)) continue;
|
||||
QueryState key = *array_get(states, i);
|
||||
uint32_t j = i;
|
||||
do {
|
||||
*array_get(states, j) = *array_get(states, j - 1);
|
||||
j--;
|
||||
} while (j > 0 && ts_query_cursor__state_precedes(self, &key, array_get(states, j - 1)));
|
||||
*array_get(states, j) = key;
|
||||
}
|
||||
}
|
||||
|
||||
static void ts_query_cursor__add_state(
|
||||
TSQueryCursor *self,
|
||||
const PatternEntry *pattern
|
||||
|
|
@ -4345,6 +4385,10 @@ static inline bool ts_query_cursor__advance(
|
|||
}
|
||||
}
|
||||
|
||||
// Order states by capture position so the dedup pass below can stop scanning a
|
||||
// group once the remaining states are disjoint from the current one.
|
||||
ts_query_cursor__sort_states_by_capture(self);
|
||||
|
||||
for (unsigned j = 0; j < self->states.size; j++) {
|
||||
QueryState *state = array_get(&self->states, j);
|
||||
if (state->dead) {
|
||||
|
|
@ -4360,7 +4404,10 @@ static inline bool ts_query_cursor__advance(
|
|||
for (unsigned k = j + 1; k < self->states.size; k++) {
|
||||
QueryState *other_state = array_get(&self->states, k);
|
||||
|
||||
// Query states are kept in ascending order of start_depth and pattern_index.
|
||||
// Query states are kept in ascending order of start_depth and pattern_index, and
|
||||
// (via the above call to `ts_query_cursor__sort_states_by_capture`) in ascending
|
||||
// order of first-capture position within each such group.
|
||||
//
|
||||
// Since the longest-match criteria is only used for deduping matches of the same
|
||||
// pattern and root node, we only need to perform pairwise comparisons within a
|
||||
// small slice of the states array.
|
||||
|
|
@ -4369,6 +4416,25 @@ static inline bool ts_query_cursor__advance(
|
|||
other_state->pattern_index != state->pattern_index
|
||||
) break;
|
||||
|
||||
// States in a group acquire their first capture in tree-traversal order, so the
|
||||
// group is ordered by first-capture position. Once `other_state`'s captures begin
|
||||
// at or after where `state`'s captures end, `other_state` (and every state after
|
||||
// it in the group) is disjoint from `state`: neither can be a capture-subset of
|
||||
// the other, so there is nothing to drop and no longest-match alternative to
|
||||
// record. Stop scanning `state` against the rest of the group.
|
||||
const CaptureList *state_captures =
|
||||
capture_list_pool_get(&self->capture_list_pool, state->capture_list_id);
|
||||
const CaptureList *other_captures =
|
||||
capture_list_pool_get(&self->capture_list_pool, other_state->capture_list_id);
|
||||
if (
|
||||
state_captures->size > 0 &&
|
||||
other_captures->size > 0 &&
|
||||
ts_node_start_byte(array_get(other_captures, 0)->node) >=
|
||||
ts_node_end_byte(array_get(state_captures, state_captures->size - 1)->node)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool left_contains_right, right_contains_left;
|
||||
ts_query_cursor__compare_captures(
|
||||
self,
|
||||
|
|
|
|||
Loading…
Reference in a new issue