Skip to content

fix(lib): reset parser options after use#4355

Merged
WillLillis merged 1 commit intotree-sitter:masterfrom
ribru17:parse_opt_fix
Apr 15, 2025
Merged

fix(lib): reset parser options after use#4355
WillLillis merged 1 commit intotree-sitter:masterfrom
ribru17:parse_opt_fix

Conversation

@ribru17
Copy link
Contributor

@ribru17 ribru17 commented Apr 12, 2025

Problem: After ts_parser_parse_with_options(), the parser options are still stored in the parser object, meaning that a successive call to ts_parser_parse() will actually behave like
ts_parser_parse_with_options(), which is not obvious and can have unintended consequences.

Solution: Reset to empty options state after
ts_parser_parse_with_options().

See neovim/neovim#33437 for real life example

@ribru17
Copy link
Contributor Author

ribru17 commented Apr 12, 2025

A question based on the way the failing tests are set up; is this behavior actually intended?

@WillLillis
Copy link
Member

WillLillis commented Apr 13, 2025

A question based on the way the failing tests are set up; is this behavior actually intended?

Reading through the failing test, I believe it is intended for state to persist between calls to ts_parse_with_options:

  • An initial parse is done with a callback that ensures it exits parsing while balancing is in progress
  • The parser is reset.
  • A fresh parse is started with the same callback as the original (intending for parsing to stop during balancing)
  • parser.parse_with_options() is called again (no reset this time), with the assumption that the parsing state was preserved during calls.
Thought I had a fix, but the neovim reproducer still fails

That being said, as far as I can tell there is no intention (or reason to) preserve the progress callback between calls. That does seem like a footgun. I tried the following change to this patch with the tree-sitter test suite as well as the neovim reproducer and both are running clean:

diff --git a/lib/src/parser.c b/lib/src/parser.c
index 71cf7bdb..49c4d440 100644
--- a/lib/src/parser.c
+++ b/lib/src/parser.c
@@ -2238,9 +2238,8 @@ TSTree *ts_parser_parse_with_options(
   self->parse_options = parse_options;
   self->parse_state.payload = parse_options.payload;
   TSTree *result = ts_parser_parse(self, old_tree, input);
-  // Reset parser state & options before further parse calls.
-  self->parse_options = (TSParseOptions) {0};
-  self->parse_state = (TSParseState) {0};
+  // Reset callback before further parse calls
+  self->parse_options.progress_callback = NULL;
   return result;
 }

(Please double check me though! It's easy to make mistakes this late at night 😅)

Reading through the original PR that added ts_parser_parse_with_options, I think it probably makes sense to reset the callback in ts_query_cursor_exec_with_options as well. However it's currently marked as const, preventing such a reassignment directly:

tree-sitter/lib/src/query.c

Lines 314 to 336 in b341073

struct TSQueryCursor {
const TSQuery *query;
TSTreeCursor cursor;
Array(QueryState) states;
Array(QueryState) finished_states;
CaptureListPool capture_list_pool;
uint32_t depth;
uint32_t max_start_depth;
uint32_t start_byte;
uint32_t end_byte;
TSPoint start_point;
TSPoint end_point;
uint32_t next_state_id;
TSClock end_clock;
TSDuration timeout_duration;
const TSQueryCursorOptions *query_options;
TSQueryCursorState query_state;
unsigned operation_count;
bool on_visible_node;
bool ascending;
bool halted;
bool did_exceed_match_limit;
};

I'm not sure what the original motivation behind this is. Probably safe to remove?

@ribru17
Copy link
Contributor Author

ribru17 commented Apr 13, 2025

Nice! Thanks, that makes a lot of sense. So the patch you gave does work, or it still fails the neovim reproducer? Also, if we clear callbacks, should the patch be changed to the following?

diff --git a/lib/src/parser.c b/lib/src/parser.c
index adcdf8d0..0429ce9a 100644
--- a/lib/src/parser.c
+++ b/lib/src/parser.c
@@ -1540,7 +1540,7 @@ static bool ts_parser__check_progress(TSParser *self, Subtree *lookahead, const
   if (self->operation_count >= OP_COUNT_PER_PARSER_TIMEOUT_CHECK) {
     self->operation_count = 0;
   }
-  if (self->parse_options.progress_callback && position != NULL) {
+  if (position != NULL) {
     self->parse_state.current_byte_offset = *position;
     self->parse_state.has_error = self->has_error;
   }
@@ -2238,6 +2238,7 @@ TSTree *ts_parser_parse_with_options(
   self->parse_options = parse_options;
   self->parse_state.payload = parse_options.payload;
   TSTree *result = ts_parser_parse(self, old_tree, input);
+  self->parse_options = (TSParseOptions) {0};
   return result;
 }

that way the parser state is kept consistent even when parsing without a callback (not sure if that is strictly necessary)

@WillLillis
Copy link
Member

So the patch you gave does work, or it still fails the neovim reproducer?

I believe it still fails the neovim reproducer. I was swapping between testing changes to neovim and tree-sitter locally, and I thought the reproducer passed (running make clean in between neovim builds), but I had a suspicion something was getting cached incorrectly. After re-cloning neovim to ensure a fully clean build with the changes the reproducer failed.

Also, if we clear callbacks, should the patch be changed to the following?

That seems correct, but I'm not 100% sure. I'll do some more testing locally to check it out :)

@clason
Copy link
Contributor

clason commented Apr 13, 2025

You need to make distclean (or manually delete the stamp file) to rebuild the dependencies in Neovim.

@WillLillis
Copy link
Member

WillLillis commented Apr 13, 2025

@ribru17 your latest patch is working for me locally! Mind pushing it up?

**Problem:** After `ts_parser_parse_with_options()`, the parser options
are still stored in the parser object, meaning that a successive call to
`ts_parser_parse()` will actually behave like
`ts_parser_parse_with_options()`, which is not obvious and can have
unintended consequences.

**Solution:** Reset to empty options state after
`ts_parser_parse_with_options()`.
@WillLillis WillLillis merged commit 733d751 into tree-sitter:master Apr 15, 2025
12 checks passed
@tree-sitter-ci-bot
Copy link

Successfully created backport PR for release-0.25:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants