Skip to content

Commit 3f424c0

Browse files
committed
feat: add an API to time out query executions
Currently, if a predicate is hard to match on the Rust side, a sizable query against a very large file can take forever, and ends up hanging. This commit adds an API function `ts_query_cursor_set_timeout_micros` to limit how long query execution is allowed to take, thereby negating the chance of a hang to occur.
1 parent a748488 commit 3f424c0

File tree

11 files changed

+132
-8
lines changed

11 files changed

+132
-8
lines changed

cli/src/tests/query_test.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5146,3 +5146,28 @@ fn test_query_on_empty_source_code() {
51465146
&[(0, vec![("program", "")])],
51475147
);
51485148
}
5149+
5150+
#[test]
5151+
fn test_query_execution_with_timeout() {
5152+
let language = get_language("javascript");
5153+
let mut parser = Parser::new();
5154+
parser.set_language(&language).unwrap();
5155+
5156+
let source_code = "function foo() { while (true) { } }\n".repeat(1000);
5157+
let tree = parser.parse(&source_code, None).unwrap();
5158+
5159+
let query = Query::new(&language, "(function_declaration) @function").unwrap();
5160+
let mut cursor = QueryCursor::new();
5161+
5162+
cursor.set_timeout_micros(1000);
5163+
let matches = cursor
5164+
.matches(&query, tree.root_node(), source_code.as_bytes())
5165+
.count();
5166+
assert!(matches < 1000);
5167+
5168+
cursor.set_timeout_micros(0);
5169+
let matches = cursor
5170+
.matches(&query, tree.root_node(), source_code.as_bytes())
5171+
.count();
5172+
assert_eq!(matches, 1000);
5173+
}

lib/binding_rust/bindings.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* automatically generated by rust-bindgen 0.69.4 */
1+
/* automatically generated by rust-bindgen 0.70.0 */
22

33
pub const TREE_SITTER_LANGUAGE_VERSION: u32 = 14;
44
pub const TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION: u32 = 13;
@@ -462,7 +462,7 @@ extern "C" {
462462
pub fn ts_tree_cursor_delete(self_: *mut TSTreeCursor);
463463
}
464464
extern "C" {
465-
#[doc = " Re-initialize a tree cursor to start at a different node."]
465+
#[doc = " Re-initialize a tree cursor to start at the original node that the cursor was\n constructed with."]
466466
pub fn ts_tree_cursor_reset(self_: *mut TSTreeCursor, node: TSNode);
467467
}
468468
extern "C" {
@@ -637,6 +637,14 @@ extern "C" {
637637
extern "C" {
638638
pub fn ts_query_cursor_set_match_limit(self_: *mut TSQueryCursor, limit: u32);
639639
}
640+
extern "C" {
641+
#[doc = " Set the maximum duration in microseconds that query execution should be allowed to\n take before halting.\n\n If query execution takes longer than this, it will halt early, returning NULL.\n See [`ts_query_cursor_next_match`] or [`ts_query_cursor_next_capture`] for more information."]
642+
pub fn ts_query_cursor_set_timeout_micros(self_: *mut TSQueryCursor, timeout_micros: u64);
643+
}
644+
extern "C" {
645+
#[doc = " Get the duration in microseconds that query execution is allowed to take."]
646+
pub fn ts_query_cursor_timeout_micros(self_: *const TSQueryCursor) -> u64;
647+
}
640648
extern "C" {
641649
#[doc = " Set the range of bytes or (row, column) positions in which the query\n will be executed."]
642650
pub fn ts_query_cursor_set_byte_range(

lib/binding_rust/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2362,6 +2362,26 @@ impl QueryCursor {
23622362
}
23632363
}
23642364

2365+
/// Set the maximum duration in microseconds that query execution should be allowed to
2366+
/// take before halting.
2367+
///
2368+
/// If query execution takes longer than this, it will halt early, returning None.
2369+
#[doc(alias = "ts_query_cursor_set_timeout_micros")]
2370+
pub fn set_timeout_micros(&mut self, timeout: u64) {
2371+
unsafe {
2372+
ffi::ts_query_cursor_set_timeout_micros(self.ptr.as_ptr(), timeout);
2373+
}
2374+
}
2375+
2376+
/// Get the duration in microseconds that query execution is allowed to take.
2377+
///
2378+
/// This is set via [`set_timeout_micros`](QueryCursor::set_timeout_micros).
2379+
#[doc(alias = "ts_query_cursor_timeout_micros")]
2380+
#[must_use]
2381+
pub fn timeout_micros(&self) -> u64 {
2382+
unsafe { ffi::ts_query_cursor_timeout_micros(self.ptr.as_ptr()) }
2383+
}
2384+
23652385
/// Check if, on its last execution, this cursor exceeded its maximum number
23662386
/// of in-progress matches.
23672387
#[doc(alias = "ts_query_cursor_did_exceed_match_limit")]

lib/binding_web/binding.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,8 @@ void ts_query_matches_wasm(
792792
uint32_t start_index,
793793
uint32_t end_index,
794794
uint32_t match_limit,
795-
uint32_t max_start_depth
795+
uint32_t max_start_depth,
796+
uint32_t timeout_micros
796797
) {
797798
if (!scratch_query_cursor) {
798799
scratch_query_cursor = ts_query_cursor_new();
@@ -810,6 +811,7 @@ void ts_query_matches_wasm(
810811
ts_query_cursor_set_byte_range(scratch_query_cursor, start_index, end_index);
811812
ts_query_cursor_set_match_limit(scratch_query_cursor, match_limit);
812813
ts_query_cursor_set_max_start_depth(scratch_query_cursor, max_start_depth);
814+
ts_query_cursor_set_timeout_micros(scratch_query_cursor, timeout_micros);
813815
ts_query_cursor_exec(scratch_query_cursor, self, node);
814816

815817
uint32_t index = 0;
@@ -847,7 +849,8 @@ void ts_query_captures_wasm(
847849
uint32_t start_index,
848850
uint32_t end_index,
849851
uint32_t match_limit,
850-
uint32_t max_start_depth
852+
uint32_t max_start_depth,
853+
uint32_t timeout_micros
851854
) {
852855
if (!scratch_query_cursor) {
853856
scratch_query_cursor = ts_query_cursor_new();
@@ -862,6 +865,7 @@ void ts_query_captures_wasm(
862865
ts_query_cursor_set_byte_range(scratch_query_cursor, start_index, end_index);
863866
ts_query_cursor_set_match_limit(scratch_query_cursor, match_limit);
864867
ts_query_cursor_set_max_start_depth(scratch_query_cursor, max_start_depth);
868+
ts_query_cursor_set_timeout_micros(scratch_query_cursor, timeout_micros);
865869
ts_query_cursor_exec(scratch_query_cursor, self, node);
866870

867871
unsigned index = 0;

lib/binding_web/binding.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,7 @@ class Query {
12791279
endIndex = 0,
12801280
matchLimit = 0xFFFFFFFF,
12811281
maxStartDepth = 0xFFFFFFFF,
1282+
timeoutMicros = 0,
12821283
} = {},
12831284
) {
12841285
if (typeof matchLimit !== 'number') {
@@ -1298,6 +1299,7 @@ class Query {
12981299
endIndex,
12991300
matchLimit,
13001301
maxStartDepth,
1302+
timeoutMicros,
13011303
);
13021304

13031305
const rawCount = getValue(TRANSFER_BUFFER, 'i32');
@@ -1342,6 +1344,7 @@ class Query {
13421344
endIndex = 0,
13431345
matchLimit = 0xFFFFFFFF,
13441346
maxStartDepth = 0xFFFFFFFF,
1347+
timeoutMicros = 0,
13451348
} = {},
13461349
) {
13471350
if (typeof matchLimit !== 'number') {
@@ -1361,6 +1364,7 @@ class Query {
13611364
endIndex,
13621365
matchLimit,
13631366
maxStartDepth,
1367+
timeoutMicros,
13641368
);
13651369

13661370
const count = getValue(TRANSFER_BUFFER, 'i32');

lib/binding_web/test/query-test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,17 @@ describe('Query', () => {
451451
]);
452452
});
453453
});
454+
455+
describe('Set a timeout', () =>
456+
it('returns less than the expected matches', () => {
457+
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
458+
query = JavaScript.query('(function_declaration name: (identifier) @function)');
459+
const matches = query.matches(tree.rootNode, { timeoutMicros: 1000 });
460+
assert.isBelow(matches.length, 1000);
461+
const matches2 = query.matches(tree.rootNode, { timeoutMicros: 0 });
462+
assert.equal(matches2.length, 1000);
463+
})
464+
);
454465
});
455466

456467
function formatMatches(matches) {

lib/binding_web/tree-sitter-web.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ declare module 'web-tree-sitter' {
179179
endIndex?: number;
180180
matchLimit?: number;
181181
maxStartDepth?: number;
182+
timeoutMicros?: number;
182183
};
183184

184185
export interface PredicateResult {

lib/include/tree_sitter/api.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,22 @@ bool ts_query_cursor_did_exceed_match_limit(const TSQueryCursor *self);
983983
uint32_t ts_query_cursor_match_limit(const TSQueryCursor *self);
984984
void ts_query_cursor_set_match_limit(TSQueryCursor *self, uint32_t limit);
985985

986+
/**
987+
* Set the maximum duration in microseconds that query execution should be allowed to
988+
* take before halting.
989+
*
990+
* If query execution takes longer than this, it will halt early, returning NULL.
991+
* See [`ts_query_cursor_next_match`] or [`ts_query_cursor_next_capture`] for more information.
992+
*/
993+
void ts_query_cursor_set_timeout_micros(TSQueryCursor *self, uint64_t timeout_micros);
994+
995+
/**
996+
* Get the duration in microseconds that query execution is allowed to take.
997+
*
998+
* This is set via [`ts_query_cursor_set_timeout_micros`].
999+
*/
1000+
uint64_t ts_query_cursor_timeout_micros(const TSQueryCursor *self);
1001+
9861002
/**
9871003
* Set the range of bytes or (row, column) positions in which the query
9881004
* will be executed.

lib/src/parser.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ static const unsigned MAX_VERSION_COUNT = 6;
8383
static const unsigned MAX_VERSION_COUNT_OVERFLOW = 4;
8484
static const unsigned MAX_SUMMARY_DEPTH = 16;
8585
static const unsigned MAX_COST_DIFFERENCE = 16 * ERROR_COST_PER_SKIPPED_TREE;
86-
static const unsigned OP_COUNT_PER_TIMEOUT_CHECK = 100;
86+
static const unsigned OP_COUNT_PER_PARSER_TIMEOUT_CHECK = 100;
8787

8888
typedef struct {
8989
Subtree token;
@@ -1565,7 +1565,7 @@ static bool ts_parser__advance(
15651565

15661566
// If a cancellation flag or a timeout was provided, then check every
15671567
// time a fixed number of parse actions has been processed.
1568-
if (++self->operation_count == OP_COUNT_PER_TIMEOUT_CHECK) {
1568+
if (++self->operation_count == OP_COUNT_PER_PARSER_TIMEOUT_CHECK) {
15691569
self->operation_count = 0;
15701570
}
15711571
if (

lib/src/query.c

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "tree_sitter/api.h"
22
#include "./alloc.h"
33
#include "./array.h"
4+
#include "./clock.h"
45
#include "./language.h"
56
#include "./point.h"
67
#include "./tree_cursor.h"
@@ -312,6 +313,9 @@ struct TSQueryCursor {
312313
TSPoint start_point;
313314
TSPoint end_point;
314315
uint32_t next_state_id;
316+
TSClock end_clock;
317+
TSDuration timeout_duration;
318+
unsigned operation_count;
315319
bool on_visible_node;
316320
bool ascending;
317321
bool halted;
@@ -322,6 +326,7 @@ static const TSQueryError PARENT_DONE = -1;
322326
static const uint16_t PATTERN_DONE_MARKER = UINT16_MAX;
323327
static const uint16_t NONE = UINT16_MAX;
324328
static const TSSymbol WILDCARD_SYMBOL = 0;
329+
static const unsigned OP_COUNT_PER_QUERY_TIMEOUT_CHECK = 100;
325330

326331
/**********
327332
* Stream
@@ -2986,6 +2991,9 @@ TSQueryCursor *ts_query_cursor_new(void) {
29862991
.start_point = {0, 0},
29872992
.end_point = POINT_MAX,
29882993
.max_start_depth = UINT32_MAX,
2994+
.timeout_duration = 0,
2995+
.end_clock = clock_null(),
2996+
.operation_count = 0,
29892997
};
29902998
array_reserve(&self->states, 8);
29912999
array_reserve(&self->finished_states, 8);
@@ -3012,6 +3020,14 @@ void ts_query_cursor_set_match_limit(TSQueryCursor *self, uint32_t limit) {
30123020
self->capture_list_pool.max_capture_list_count = limit;
30133021
}
30143022

3023+
uint64_t ts_query_cursor_timeout_micros(const TSQueryCursor *self) {
3024+
return duration_to_micros(self->timeout_duration);
3025+
}
3026+
3027+
void ts_query_cursor_set_timeout_micros(TSQueryCursor *self, uint64_t timeout_micros) {
3028+
self->timeout_duration = duration_from_micros(timeout_micros);
3029+
}
3030+
30153031
#ifdef DEBUG_EXECUTE_QUERY
30163032
#define LOG(...) fprintf(stderr, __VA_ARGS__)
30173033
#else
@@ -3023,7 +3039,7 @@ void ts_query_cursor_exec(
30233039
const TSQuery *query,
30243040
TSNode node
30253041
) {
3026-
if (query) {
3042+
if (query) {
30273043
LOG("query steps:\n");
30283044
for (unsigned i = 0; i < query->steps.size; i++) {
30293045
QueryStep *step = &query->steps.contents[i];
@@ -3060,6 +3076,12 @@ void ts_query_cursor_exec(
30603076
self->halted = false;
30613077
self->query = query;
30623078
self->did_exceed_match_limit = false;
3079+
self->operation_count = 0;
3080+
if (self->timeout_duration) {
3081+
self->end_clock = clock_after(clock_now(), self->timeout_duration);
3082+
} else {
3083+
self->end_clock = clock_null();
3084+
}
30633085
}
30643086

30653087
void ts_query_cursor_set_byte_range(
@@ -3456,7 +3478,19 @@ static inline bool ts_query_cursor__advance(
34563478
}
34573479
}
34583480

3459-
if (did_match || self->halted) return did_match;
3481+
if (++self->operation_count == OP_COUNT_PER_QUERY_TIMEOUT_CHECK) {
3482+
self->operation_count = 0;
3483+
}
3484+
if (
3485+
did_match ||
3486+
self->halted ||
3487+
(
3488+
self->operation_count == 0 &&
3489+
!clock_is_null(self->end_clock) && clock_is_gt(clock_now(), self->end_clock)
3490+
)
3491+
) {
3492+
return did_match;
3493+
}
34603494

34613495
// Exit the current node.
34623496
if (self->ascending) {

0 commit comments

Comments
 (0)