Skip to content

Commit 5eae036

Browse files
committed
refine Query / QueryCursor separation
1 parent 4426ee5 commit 5eae036

File tree

3 files changed

+66
-15
lines changed

3 files changed

+66
-15
lines changed

src/main/java/io/github/treesitter/jtreesitter/Query.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,10 +478,21 @@ public Map<String, Optional<String>> getPatternAssertions(@Unsigned int index, b
478478
}
479479

480480

481+
/**
482+
* Execute the query on a given node.
483+
* @param node The node that the query will run on.
484+
* @return A cursor that can be used to iterate over the matches.
485+
*/
481486
public QueryCursor execute(Node node){
482487
return new QueryCursor(this, node, cursorOptions);
483488
}
484489

490+
/**
491+
* Execute the query on a given node with the given options. The options override the default options set on the query.
492+
* @param node The node that the query will run on.
493+
* @param options The options that will be used for this query.
494+
* @return A cursor that can be used to iterate over the matches.
495+
*/
485496
public QueryCursor execute(Node node, QueryCursorOptions options){
486497
return new QueryCursor(this, node, options);
487498
}
@@ -504,19 +515,20 @@ public Stream<QueryMatch> findMatches(Node node) {
504515
*
505516
* <h4 id="findMatches-example">Predicate Example</h4>
506517
* <p>
507-
* {@snippet lang="java" :
518+
* {@snippet lang = "java":
508519
* Stream<QueryMatch> matches = query.findMatches(tree.getRootNode(), (predicate, match) -> {
509520
* if (!predicate.getName().equals("ieq?")) return true;
510521
* List<QueryPredicateArg> args = predicate.getArgs();
511522
* Node node = match.findNodes(args.getFirst().value()).getFirst();
512523
* return args.getLast().value().equalsIgnoreCase(node.getText());
513524
* });
514-
* }
525+
*}
515526
*
516-
* @param node The node that the query will run on.
527+
* @param node The node that the query will run on.
517528
* @param predicate A function that handles custom predicates.
529+
* @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#stream(BiPredicate)}.
518530
*/
519-
public Stream<QueryMatch> findMatches(Node node, @Nullable BiPredicate<QueryPredicate, QueryMatch> predicate){
531+
public Stream<QueryMatch> findMatches(Node node, @Nullable BiPredicate<QueryPredicate, QueryMatch> predicate) {
520532
return findMatches(node, predicate, arena);
521533
}
522534

@@ -531,7 +543,9 @@ public Stream<QueryMatch> findMatches(Node node, @Nullable BiPredicate<QueryPred
531543
*/
532544
public Stream<QueryMatch> findMatches(Node node, @Nullable BiPredicate<QueryPredicate, QueryMatch> predicate, SegmentAllocator allocator) {
533545
try(QueryCursor cursor = this.execute(node)){
534-
return cursor.stream(allocator, predicate);
546+
// make sure to load the entire stream into memory before closing the cursor.
547+
// Otherwise, we call for nextMatch after closing the cursor which leads to undefined behavior.
548+
return cursor.stream(allocator, predicate).toList().stream();
535549
}
536550
}
537551

src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ public class QueryCursor implements AutoCloseable{
3737
private final Arena arena;
3838

3939
private final Query query;
40-
private final Node cursorRootNode;
40+
private final Tree tree;
4141

4242

4343
QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorOptions options){
4444
arena = Arena.ofConfined();
4545
cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete);
4646
this.query = query;
47-
this.cursorRootNode = cursorRootNode;
47+
this.tree = cursorRootNode.getTree();
4848
if(options != null){
4949
applyOptions(options);
5050
}
@@ -79,7 +79,6 @@ private void applyOptions(QueryCursorOptions options){
7979
if (options.getTimeoutMicros() >= 0) {
8080
ts_query_cursor_set_timeout_micros(cursor, options.getTimeoutMicros());
8181
}
82-
8382
}
8483

8584

@@ -117,30 +116,61 @@ public void removeMatch(@Unsigned int matchId){
117116
ts_query_cursor_remove_match(cursor, matchId);
118117
}
119118

119+
/**
120+
* Stream the matches produced by the query. The stream can not be consumed after the cursor is closed. The native
121+
* nodes backing the matches are bound to the lifetime of the cursor.
122+
* @return a stream of matches
123+
*/
120124
public Stream<QueryMatch> stream() {
121125
return stream(null);
122126
}
123127

128+
/**
129+
* Like {@link #stream()} but allows for custom predicates to be applied to the matches.
130+
* @param predicate a function to handle custom predicates.
131+
* @return a stream of matches
132+
*/
124133
public Stream<QueryMatch> stream(@Nullable BiPredicate<QueryPredicate, QueryMatch> predicate) {
125134
return stream(arena , predicate);
126135
}
127136

137+
/**
138+
* Like {@link #stream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes.
139+
* @param allocator allocator to use for allocating the native nodes backing the matches
140+
* @param predicate a function to handle custom predicates.
141+
* @return a stream of matches
142+
*/
128143
public Stream<QueryMatch> stream(SegmentAllocator allocator, @Nullable BiPredicate<QueryPredicate, QueryMatch> predicate) {
129144
return StreamSupport.stream(new MatchesIterator(this, allocator, predicate), false);
130145
}
131146

132147

148+
/**
149+
* Get the next match produced by the query. The native nodes backing the match are bound to the lifetime of the cursor.
150+
* @return the next match, if available
151+
*/
133152
public Optional<QueryMatch> nextMatch() {
134153
return nextMatch(null);
135154
}
136155

156+
/**
157+
* Like {@link #nextMatch()} but allows for custom predicates to be applied to the matches.
158+
* @param predicate a function to handle custom predicates.
159+
* @return the next match, if available
160+
*/
137161
public Optional<QueryMatch> nextMatch(@Nullable BiPredicate<QueryPredicate, QueryMatch> predicate) {
138162
return nextMatch(arena, predicate);
139163
}
140164

165+
/**
166+
* Like {@link #nextMatch(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes.
167+
* @param allocator allocator to use for allocating the native nodes backing the matches
168+
* @param predicate a function to handle custom predicates.
169+
* @return the next match, if available
170+
*/
141171
public Optional<QueryMatch> nextMatch(SegmentAllocator allocator, @Nullable BiPredicate<QueryPredicate, QueryMatch> predicate) {
142172

143-
var hasNoText = cursorRootNode.getTree().getText() == null;
173+
var hasNoText = tree.getText() == null;
144174
MemorySegment match = arena.allocate(TSQueryMatch.layout());
145175
while (ts_query_cursor_next_match(cursor, match)) {
146176
var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match));
@@ -150,11 +180,13 @@ public Optional<QueryMatch> nextMatch(SegmentAllocator allocator, @Nullable BiPr
150180
var capture = TSQueryCapture.asSlice(matchCaptures, i);
151181
var name = query.getCaptureNames().get(TSQueryCapture.index(capture));
152182
var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture));
153-
captureList.add(new QueryCapture(name, new Node(node, this.cursorRootNode.getTree())));
183+
captureList.add(new QueryCapture(name, new Node(node, tree)));
154184
}
155185
var patternIndex = TSQueryMatch.pattern_index(match);
156186
var matchId = TSQueryMatch.id(match);
157-
var result = new QueryMatch(matchId, patternIndex, captureList);
187+
// we copy all the data. So we can directly remove the match from the cursor to free memory.
188+
ts_query_cursor_remove_match(cursor, matchId);
189+
var result = new QueryMatch(patternIndex, captureList);
158190
if (hasNoText || matches(predicate, result)) {
159191
return Optional.of(result);
160192
}
@@ -190,6 +222,11 @@ public MatchesIterator(QueryCursor cursor, SegmentAllocator allocator, @Nullable
190222
}
191223
@Override
192224
public boolean tryAdvance(Consumer<? super QueryMatch> action) {
225+
226+
if(!cursor.arena.scope().isAlive()){
227+
throw new IllegalStateException("Cursor is closed. Cannot produce more matches.");
228+
}
229+
193230
Optional<QueryMatch> queryMatch = cursor.nextMatch(allocator, predicate);
194231
queryMatch.ifPresent(action);
195232
return queryMatch.isPresent();

src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
/** A match that corresponds to a certain pattern in the query. */
77
@NullMarked
8-
public record QueryMatch(int matchId, @Unsigned int patternIndex, List<QueryCapture> captures) {
8+
public record QueryMatch(@Unsigned int patternIndex, List<QueryCapture> captures) {
99
/** Creates an instance of a QueryMatch record class. */
10-
public QueryMatch(@Unsigned int matchId, @Unsigned int patternIndex, List<QueryCapture> captures) {
11-
this.matchId = matchId;
10+
public QueryMatch(@Unsigned int patternIndex, List<QueryCapture> captures) {
11+
1212
this.patternIndex = patternIndex;
1313
this.captures = List.copyOf(captures);
1414
}
@@ -24,6 +24,6 @@ public List<Node> findNodes(String capture) {
2424
@Override
2525
public String toString() {
2626
return String.format(
27-
"QueryMatch[matchId=%s,patternIndex=%s, captures=%s]", Integer.toUnsignedString(matchId), Integer.toUnsignedString(patternIndex), captures);
27+
"QueryMatch[patternIndex=%s, captures=%s]", Integer.toUnsignedString(patternIndex), captures);
2828
}
2929
}

0 commit comments

Comments
 (0)