diff --git a/.github/scripts/wasm_stdlib.js b/.github/scripts/wasm_stdlib.js new file mode 100644 index 00000000..e1350094 --- /dev/null +++ b/.github/scripts/wasm_stdlib.js @@ -0,0 +1,25 @@ +module.exports = async ({ github, context, core }) => { + if (context.eventName !== 'pull_request') return; + + const prNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + + const { data: files } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number: prNumber + }); + + const changedFiles = files.map(file => file.filename); + + const wasmStdLibSrc = 'crates/language/wasm/'; + const dirChanged = changedFiles.some(file => file.startsWith(wasmStdLibSrc)); + + if (!dirChanged) return; + + const wasmStdLibHeader = 'lib/src/wasm/wasm-stdlib.h'; + const requiredChanged = changedFiles.includes(wasmStdLibHeader); + + if (!requiredChanged) core.setFailed(`Changes detected in ${wasmStdLibSrc} but ${wasmStdLibHeader} was not modified.`); +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97a7e378..a60c93f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,3 +44,6 @@ jobs: build: uses: ./.github/workflows/build.yml + + check-wasm-stdlib: + uses: ./.github/workflows/wasm_stdlib.yml diff --git a/.github/workflows/wasm_stdlib.yml b/.github/workflows/wasm_stdlib.yml new file mode 100644 index 00000000..adec8411 --- /dev/null +++ b/.github/workflows/wasm_stdlib.yml @@ -0,0 +1,19 @@ +name: Check Wasm Stdlib build + +on: + workflow_call: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check directory changes + uses: actions/github-script@v8 + with: + script: | + const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`; + const script = require(scriptPath); + return script({ github, context, core }); diff --git a/Cargo.lock b/Cargo.lock index 4bb4214f..ae7803c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "shlex", @@ -664,9 +664,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fnv" diff --git a/Cargo.toml b/Cargo.toml index 5730ddf2..ca0d644a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ ansi_colours = "1.2.3" anstyle = "1.0.13" anyhow = "1.0.100" bstr = "1.12.0" -cc = "1.2.52" +cc = "1.2.53" clap = { version = "4.5.54", features = [ "cargo", "derive", diff --git a/crates/cli/src/parse.rs b/crates/cli/src/parse.rs index 1a1d9723..3551f556 100644 --- a/crates/cli/src/parse.rs +++ b/crates/cli/src/parse.rs @@ -953,7 +953,7 @@ fn render_node_range( fn cst_render_node( opts: &ParseFileOptions, - cursor: &mut TreeCursor, + cursor: &TreeCursor, source_code: &[u8], out: &mut impl Write, total_width: usize, diff --git a/crates/cli/src/playground.html b/crates/cli/src/playground.html index 6f90e030..147516ac 100644 --- a/crates/cli/src/playground.html +++ b/crates/cli/src/playground.html @@ -19,7 +19,8 @@ --light-scrollbar-track: #f1f1f1; --light-scrollbar-thumb: #c1c1c1; --light-scrollbar-thumb-hover: #a8a8a8; - + --light-tree-row-bg: #e3f2fd; + --dark-bg: #1d1f21; --dark-border: #2d2d2d; --dark-text: #c5c8c6; @@ -28,6 +29,7 @@ --dark-scrollbar-track: #25282c; --dark-scrollbar-thumb: #4a4d51; --dark-scrollbar-thumb-hover: #5a5d61; + --dark-tree-row-bg: #373737; --primary-color: #0550ae; --primary-color-alpha: rgba(5, 80, 174, 0.1); @@ -42,6 +44,7 @@ --text-color: var(--dark-text); --panel-bg: var(--dark-panel-bg); --code-bg: var(--dark-code-bg); + --tree-row-bg: var(--dark-tree-row-bg); } [data-theme="light"] { @@ -50,6 +53,7 @@ --text-color: var(--light-text); --panel-bg: white; --code-bg: white; + --tree-row-bg: var(--light-tree-row-bg); } /* Base Styles */ @@ -275,7 +279,7 @@ } #output-container a.highlighted { - background-color: #d9d9d9; + background-color: #cae2ff; color: red; border-radius: 3px; text-decoration: underline; @@ -346,7 +350,7 @@ } & #output-container a.highlighted { - background-color: #373b41; + background-color: #656669; color: red; } @@ -373,6 +377,9 @@ color: var(--dark-text); } } + .tree-row:has(.highlighted) { + background-color: var(--tree-row-bg); + } diff --git a/crates/cli/src/tests/helpers/query_helpers.rs b/crates/cli/src/tests/helpers/query_helpers.rs index e648ac8e..e2c68f17 100644 --- a/crates/cli/src/tests/helpers/query_helpers.rs +++ b/crates/cli/src/tests/helpers/query_helpers.rs @@ -225,7 +225,7 @@ impl Pattern { } // Find every matching combination of child patterns and child nodes. - let mut finished_matches = Vec::::new(); + let mut finished_matches = Vec::>::new(); if cursor.goto_first_child() { let mut match_states = vec![(0, mat)]; loop { diff --git a/crates/generate/src/quickjs.rs b/crates/generate/src/quickjs.rs index 99e77397..d8c71cfe 100644 --- a/crates/generate/src/quickjs.rs +++ b/crates/generate/src/quickjs.rs @@ -215,11 +215,11 @@ fn try_resolve_path(path: &Path) -> rquickjs::Result { } #[allow(clippy::needless_pass_by_value)] -fn require_from_module<'a>( - ctx: Ctx<'a>, +fn require_from_module<'js>( + ctx: Ctx<'js>, module_path: String, from_module: &str, -) -> rquickjs::Result> { +) -> rquickjs::Result> { let current_module = PathBuf::from(from_module); let current_dir = if current_module.is_file() { current_module.parent().unwrap_or(Path::new(".")) @@ -234,13 +234,13 @@ fn require_from_module<'a>( load_module_from_content(&ctx, &resolved_path, &contents) } -fn load_module_from_content<'a>( - ctx: &Ctx<'a>, +fn load_module_from_content<'js>( + ctx: &Ctx<'js>, path: &Path, contents: &str, -) -> rquickjs::Result> { +) -> rquickjs::Result> { if path.extension().is_some_and(|ext| ext == "json") { - return ctx.eval::(format!("JSON.parse({contents:?})")); + return ctx.eval::, _>(format!("JSON.parse({contents:?})")); } let exports = Object::new(ctx.clone())?; @@ -256,7 +256,7 @@ fn load_module_from_content<'a>( let module_path = filename.clone(); let require = Function::new( ctx.clone(), - move |ctx_inner: Ctx<'a>, target_path: String| -> rquickjs::Result> { + move |ctx_inner: Ctx<'js>, target_path: String| -> rquickjs::Result> { require_from_module(ctx_inner, target_path, &module_path) }, )?; @@ -264,8 +264,8 @@ fn load_module_from_content<'a>( let wrapper = format!("(function(exports, require, module, __filename, __dirname) {{ {contents} }})"); - let module_func = ctx.eval::(wrapper)?; - module_func.call::<_, Value>((exports, require, module_obj.clone(), filename, dirname))?; + let module_func = ctx.eval::, _>(wrapper)?; + module_func.call::<_, Value<'js>>((exports, require, module_obj.clone(), filename, dirname))?; module_obj.get("exports") } diff --git a/crates/highlight/src/highlight.rs b/crates/highlight/src/highlight.rs index 9a78d1ac..d38351f6 100644 --- a/crates/highlight/src/highlight.rs +++ b/crates/highlight/src/highlight.rs @@ -189,7 +189,7 @@ struct HighlightIterLayer<'a> { depth: usize, } -pub struct _QueryCaptures<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> { +pub struct _QueryCaptures<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> { ptr: *mut ffi::TSQueryCursor, query: &'query Query, text_provider: T, @@ -225,7 +225,7 @@ impl<'tree> _QueryMatch<'_, 'tree> { } } -impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> Iterator +impl<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> Iterator for _QueryCaptures<'query, 'tree, T, I> { type Item = (QueryMatch<'query, 'tree>, usize); @@ -594,6 +594,7 @@ impl<'a> HighlightIterLayer<'a> { } } + // SAFETY: // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // prevents them from being moved. But both of these values are really just // pointers, so it's actually ok to move them. diff --git a/crates/loader/src/loader.rs b/crates/loader/src/loader.rs index 11c8b673..e7584378 100644 --- a/crates/loader/src/loader.rs +++ b/crates/loader/src/loader.rs @@ -765,7 +765,7 @@ impl Loader { } #[must_use] - pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration, &Path)> { + pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration<'static>, &Path)> { self.language_configurations .iter() .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref())) @@ -775,7 +775,7 @@ impl Loader { pub fn language_configuration_for_scope( &self, scope: &str, - ) -> LoaderResult> { + ) -> LoaderResult)>> { for configuration in &self.language_configurations { if configuration.scope.as_ref().is_some_and(|s| s == scope) { let language = self.language_for_id(configuration.language_id)?; @@ -788,7 +788,7 @@ impl Loader { pub fn language_configuration_for_first_line_regex( &self, path: &Path, - ) -> LoaderResult> { + ) -> LoaderResult)>> { self.language_configuration_ids_by_first_line_regex .iter() .try_fold(None, |_, (regex, ids)| { @@ -817,7 +817,7 @@ impl Loader { pub fn language_configuration_for_file_name( &self, path: &Path, - ) -> LoaderResult> { + ) -> LoaderResult)>> { // Find all the language configurations that match this file name // or a suffix of the file name. let configuration_ids = path @@ -889,7 +889,7 @@ impl Loader { pub fn language_configuration_for_injection_string( &self, string: &str, - ) -> LoaderResult> { + ) -> LoaderResult)>> { let mut best_match_length = 0; let mut best_match_position = None; for (i, configuration) in self.language_configurations.iter().enumerate() { @@ -1305,6 +1305,11 @@ impl Loader { })); } } + } else { + warn!( + "Failed to run `nm` to verify symbols in {}", + library_path.display() + ); } Ok(()) @@ -1534,7 +1539,9 @@ impl Loader { } #[must_use] - pub fn get_language_configuration_in_current_path(&self) -> Option<&LanguageConfiguration> { + pub fn get_language_configuration_in_current_path( + &self, + ) -> Option<&LanguageConfiguration<'static>> { self.language_configuration_in_current_path .map(|i| &self.language_configurations[i]) } @@ -1543,7 +1550,7 @@ impl Loader { &mut self, parser_path: &Path, set_current_path_config: bool, - ) -> LoaderResult<&[LanguageConfiguration]> { + ) -> LoaderResult<&[LanguageConfiguration<'static>]> { let initial_language_configuration_count = self.language_configurations.len(); match TreeSitterJSON::from_file(parser_path) { diff --git a/crates/tags/src/tags.rs b/crates/tags/src/tags.rs index 16270b0a..c6654876 100644 --- a/crates/tags/src/tags.rs +++ b/crates/tags/src/tags.rs @@ -313,6 +313,7 @@ impl TagsContext { ) .ok_or(Error::Cancelled)?; + // SAFETY: // The `matches` iterator borrows the `Tree`, which prevents it from being // moved. But the tree is really just a pointer, so it's actually ok to // move it. diff --git a/lib/binding_rust/lib.rs b/lib/binding_rust/lib.rs index bf86cf74..51fd7f25 100644 --- a/lib/binding_rust/lib.rs +++ b/lib/binding_rust/lib.rs @@ -317,7 +317,7 @@ pub trait Decode { /// A stateful object for walking a syntax [`Tree`] efficiently. #[doc(alias = "TSTreeCursor")] -pub struct TreeCursor<'cursor>(ffi::TSTreeCursor, PhantomData<&'cursor ()>); +pub struct TreeCursor<'tree>(ffi::TSTreeCursor, PhantomData<&'tree ()>); /// A set of patterns that match nodes in a syntax tree. #[doc(alias = "TSQuery")] @@ -392,7 +392,7 @@ pub struct QueryMatch<'cursor, 'tree> { } /// A sequence of [`QueryMatch`]es associated with a given [`QueryCursor`]. -pub struct QueryMatches<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> { +pub struct QueryMatches<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> { ptr: *mut ffi::TSQueryCursor, query: &'query Query, text_provider: T, @@ -407,7 +407,7 @@ pub struct QueryMatches<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8] /// /// During iteration, each element contains a [`QueryMatch`] and index. The index can /// be used to access the new capture inside of the [`QueryMatch::captures`]'s [`captures`]. -pub struct QueryCaptures<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> { +pub struct QueryCaptures<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> { ptr: *mut ffi::TSQueryCursor, query: &'query Query, text_provider: T, @@ -1581,7 +1581,7 @@ impl<'tree> Node<'tree> { /// Get the [`Language`] that was used to parse this node's syntax tree. #[doc(alias = "ts_node_language")] #[must_use] - pub fn language(&self) -> LanguageRef { + pub fn language(&self) -> LanguageRef<'tree> { LanguageRef(unsafe { ffi::ts_node_language(self.0) }, PhantomData) } @@ -2082,11 +2082,11 @@ impl fmt::Display for Node<'_> { } } -impl<'cursor> TreeCursor<'cursor> { +impl<'tree> TreeCursor<'tree> { /// Get the tree cursor's current [`Node`]. #[doc(alias = "ts_tree_cursor_current_node")] #[must_use] - pub fn node(&self) -> Node<'cursor> { + pub fn node(&self) -> Node<'tree> { Node( unsafe { ffi::ts_tree_cursor_current_node(&self.0) }, PhantomData, @@ -2227,7 +2227,7 @@ impl<'cursor> TreeCursor<'cursor> { /// Re-initialize this tree cursor to start at the original node that the /// cursor was constructed with. #[doc(alias = "ts_tree_cursor_reset")] - pub fn reset(&mut self, node: Node<'cursor>) { + pub fn reset(&mut self, node: Node<'tree>) { unsafe { ffi::ts_tree_cursor_reset(&mut self.0, node.0) }; } @@ -3404,7 +3404,7 @@ impl QueryProperty { /// Provide a `StreamingIterator` instead of the traditional `Iterator`, as the /// underlying object in the C library gets updated on each iteration. Copies would /// have their internal state overwritten, leading to Undefined Behavior -impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIterator +impl<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> StreamingIterator for QueryMatches<'query, 'tree, T, I> { type Item = QueryMatch<'query, 'tree>; @@ -3435,15 +3435,13 @@ impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIterato } } -impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIteratorMut - for QueryMatches<'query, 'tree, T, I> -{ +impl, I: AsRef<[u8]>> StreamingIteratorMut for QueryMatches<'_, '_, T, I> { fn get_mut(&mut self) -> Option<&mut Self::Item> { self.current_match.as_mut() } } -impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIterator +impl<'query, 'tree, T: TextProvider, I: AsRef<[u8]>> StreamingIterator for QueryCaptures<'query, 'tree, T, I> { type Item = (QueryMatch<'query, 'tree>, usize); @@ -3480,9 +3478,7 @@ impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIterato } } -impl<'query, 'tree: 'query, T: TextProvider, I: AsRef<[u8]>> StreamingIteratorMut - for QueryCaptures<'query, 'tree, T, I> -{ +impl, I: AsRef<[u8]>> StreamingIteratorMut for QueryCaptures<'_, '_, T, I> { fn get_mut(&mut self) -> Option<&mut Self::Item> { self.current_match.as_mut() } @@ -3622,8 +3618,8 @@ impl From for Range { } } -impl From<&'_ InputEdit> for ffi::TSInputEdit { - fn from(val: &'_ InputEdit) -> Self { +impl From<&InputEdit> for ffi::TSInputEdit { + fn from(val: &InputEdit) -> Self { Self { start_byte: val.start_byte as u32, old_end_byte: val.old_end_byte as u32, diff --git a/lib/binding_web/src/finalization_registry.ts b/lib/binding_web/src/finalization_registry.ts new file mode 100644 index 00000000..5f9c45cc --- /dev/null +++ b/lib/binding_web/src/finalization_registry.ts @@ -0,0 +1,8 @@ +export function newFinalizer(handler: (value: T) => void): FinalizationRegistry | undefined { + try { + return new FinalizationRegistry(handler); + } catch(e) { + console.error('Unsupported FinalizationRegistry:', e); + return; + } +} diff --git a/lib/binding_web/src/lookahead_iterator.ts b/lib/binding_web/src/lookahead_iterator.ts index 92b4d28f..4dd6b296 100644 --- a/lib/binding_web/src/lookahead_iterator.ts +++ b/lib/binding_web/src/lookahead_iterator.ts @@ -1,5 +1,10 @@ import { C, Internal, assertInternal } from './constants'; import { Language } from './language'; +import { newFinalizer } from './finalization_registry'; + +const finalizer = newFinalizer((address: number) => { + C._ts_lookahead_iterator_delete(address); +}); export class LookaheadIterator implements Iterable { /** @internal */ @@ -13,6 +18,7 @@ export class LookaheadIterator implements Iterable { assertInternal(internal); this[0] = address; this.language = language; + finalizer?.register(this, address, this); } /** Get the current symbol of the lookahead iterator. */ @@ -27,6 +33,7 @@ export class LookaheadIterator implements Iterable { /** Delete the lookahead iterator, freeing its resources. */ delete(): void { + finalizer?.unregister(this); C._ts_lookahead_iterator_delete(this[0]); this[0] = 0; } diff --git a/lib/binding_web/src/parser.ts b/lib/binding_web/src/parser.ts index efcadf05..7e3c3b4a 100644 --- a/lib/binding_web/src/parser.ts +++ b/lib/binding_web/src/parser.ts @@ -3,6 +3,7 @@ import { Language } from './language'; import { marshalRange, unmarshalRange } from './marshal'; import { checkModule, initializeBinding } from './bindings'; import { Tree } from './tree'; +import { newFinalizer } from './finalization_registry'; /** * Options for parsing @@ -82,6 +83,11 @@ export let LANGUAGE_VERSION: number; */ export let MIN_COMPATIBLE_VERSION: number; +const finalizer = newFinalizer((addresses: number[]) => { + C._ts_parser_delete(addresses[0]); + C._free(addresses[1]); +}); + /** * A stateful object that is used to produce a {@link Tree} based on some * source code. @@ -117,6 +123,7 @@ export class Parser { */ constructor() { this.initialize(); + finalizer?.register(this, [this[0], this[1]], this); } /** @internal */ @@ -131,6 +138,7 @@ export class Parser { /** Delete the parser, freeing its resources. */ delete() { + finalizer?.unregister(this); C._ts_parser_delete(this[0]); C._free(this[1]); this[0] = 0; diff --git a/lib/binding_web/src/query.ts b/lib/binding_web/src/query.ts index b9cd1971..e2994b14 100644 --- a/lib/binding_web/src/query.ts +++ b/lib/binding_web/src/query.ts @@ -3,6 +3,7 @@ import { Node } from './node'; import { marshalNode, unmarshalCaptures } from './marshal'; import { TRANSFER_BUFFER } from './parser'; import { Language } from './language'; +import { newFinalizer } from './finalization_registry'; const PREDICATE_STEP_TYPE_CAPTURE = 1; @@ -506,6 +507,10 @@ function parsePattern( } } +const finalizer = newFinalizer((address: number) => { + C._ts_query_delete(address); +}); + export class Query { /** @internal */ private [0] = 0; // Internal handle for Wasm @@ -687,10 +692,12 @@ export class Query { this.assertedProperties = assertedProperties; this.refutedProperties = refutedProperties; this.exceededMatchLimit = false; + finalizer?.register(this, address, this); } /** Delete the query, freeing its resources. */ delete(): void { + finalizer?.unregister(this); C._ts_query_delete(this[0]); this[0] = 0; } diff --git a/lib/binding_web/src/tree.ts b/lib/binding_web/src/tree.ts index 7a251440..f6a7aaf3 100644 --- a/lib/binding_web/src/tree.ts +++ b/lib/binding_web/src/tree.ts @@ -5,6 +5,7 @@ import { TreeCursor } from './tree_cursor'; import { marshalEdit, marshalPoint, unmarshalNode, unmarshalRange } from './marshal'; import { TRANSFER_BUFFER } from './parser'; import { Edit } from './edit'; +import { newFinalizer } from './finalization_registry'; /** @internal */ export function getText(tree: Tree, startIndex: number, endIndex: number, startPosition: Point): string { @@ -28,6 +29,10 @@ export function getText(tree: Tree, startIndex: number, endIndex: number, startP return result ?? ''; } +const finalizer = newFinalizer((address: number) => { + C._ts_tree_delete(address); +}); + /** A tree that represents the syntactic structure of a source code file. */ export class Tree { /** @internal */ @@ -45,6 +50,7 @@ export class Tree { this[0] = address; this.language = language; this.textCallback = textCallback; + finalizer?.register(this, address, this); } /** Create a shallow copy of the syntax tree. This is very fast. */ @@ -55,6 +61,7 @@ export class Tree { /** Delete the syntax tree, freeing its resources. */ delete(): void { + finalizer?.unregister(this); C._ts_tree_delete(this[0]); this[0] = 0; } diff --git a/lib/binding_web/src/tree_cursor.ts b/lib/binding_web/src/tree_cursor.ts index 7562bb7f..978a86dc 100644 --- a/lib/binding_web/src/tree_cursor.ts +++ b/lib/binding_web/src/tree_cursor.ts @@ -3,6 +3,11 @@ import { marshalNode, marshalPoint, marshalTreeCursor, unmarshalNode, unmarshalP import { Node } from './node'; import { TRANSFER_BUFFER } from './parser'; import { getText, Tree } from './tree'; +import { newFinalizer } from './finalization_registry'; + +const finalizer = newFinalizer((address: number) => { + C._ts_tree_cursor_delete_wasm(address); +}); /** A stateful object for walking a syntax {@link Tree} efficiently. */ export class TreeCursor { @@ -30,6 +35,7 @@ export class TreeCursor { assertInternal(internal); this.tree = tree; unmarshalTreeCursor(this); + finalizer?.register(this, this.tree[0], this); } /** Creates a deep copy of the tree cursor. This allocates new memory. */ @@ -42,6 +48,7 @@ export class TreeCursor { /** Delete the tree cursor, freeing its resources. */ delete(): void { + finalizer?.unregister(this); marshalTreeCursor(this); C._ts_tree_cursor_delete_wasm(this.tree[0]); this[0] = this[1] = this[2] = 0; diff --git a/lib/binding_web/test/memory.test.ts b/lib/binding_web/test/memory.test.ts new file mode 100644 index 00000000..46238934 --- /dev/null +++ b/lib/binding_web/test/memory.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import { gc, event, Finalizer } from './memory'; + +// hijack finalization registry before import web-tree-sitter +globalThis.FinalizationRegistry = Finalizer; + +describe('Memory Management', () => { + describe('call .delete()', () => { + it('test free memory manually', async () => { + const timer = setInterval(() => { + gc(); + }, 100); + let done = 0; + event.on('gc', () => { + done++; + }); + await (async () => { + const { JavaScript } = await (await import('./helper')).default; + const { Parser, Query } = await import('../src'); + const parser = new Parser(); + parser.setLanguage(JavaScript); + const tree = parser.parse('1+1')!; + const copyTree = tree.copy(); + const cursor = tree.walk(); + const copyCursor = cursor.copy(); + const lookaheadIterator = JavaScript.lookaheadIterator(cursor.currentNode.nextParseState)!; + const query = new Query(JavaScript, '(identifier) @element'); + parser.delete(); + tree.delete(); + copyTree.delete(); + cursor.delete(); + copyCursor.delete(); + lookaheadIterator.delete(); + query.delete(); + })(); + // wait for gc + await new Promise((resolve) => setTimeout(resolve, 1000)); + clearInterval(timer); + // expect no gc event fired + expect(done).toBe(0); + }); + }); + + describe('do not call .delete()', () => { + it('test free memory automatically', async () => { + const timer = setInterval(() => { + gc(); + }, 100); + let done = 0; + const promise = new Promise((resolve) => { + event.on('gc', () => { + if (++done === 7) { + resolve(undefined); + clearInterval(timer); + } + console.log('free memory times: ', done); + }); + }); + await (async () => { + const { JavaScript } = await (await import('./helper')).default; + const { Parser, Query } = await import('../src'); + const parser = new Parser(); // 1 + parser.setLanguage(JavaScript); + const tree = parser.parse('1+1')!; // 2 + tree.copy(); // 3 + const cursor = tree.walk(); // 4 + cursor.copy(); // 5 + JavaScript.lookaheadIterator(cursor.currentNode.nextParseState)!; // 6 + new Query(JavaScript, '(identifier) @element'); // 7 + })(); + await promise; + }); + }); +}); diff --git a/lib/binding_web/test/memory.ts b/lib/binding_web/test/memory.ts new file mode 100644 index 00000000..62cb8b7d --- /dev/null +++ b/lib/binding_web/test/memory.ts @@ -0,0 +1,20 @@ +import { EventEmitter } from 'events'; +import { Session } from 'inspector'; + +const session = new Session(); +session.connect(); + +export function gc() { + session.post('HeapProfiler.collectGarbage'); +} + +export const event = new EventEmitter(); + +export class Finalizer extends FinalizationRegistry { + constructor(handler: (value: T) => void) { + super((value) => { + handler(value); + event.emit('gc'); + }); + } +} diff --git a/lib/binding_web/test/memory_unsupported.test.ts b/lib/binding_web/test/memory_unsupported.test.ts new file mode 100644 index 00000000..cc1f69bf --- /dev/null +++ b/lib/binding_web/test/memory_unsupported.test.ts @@ -0,0 +1,25 @@ +import { describe, it } from 'vitest'; + +describe('FinalizationRegistry is unsupported', () => { + it('test FinalizationRegistry is unsupported', async () => { + // @ts-expect-error: test FinalizationRegistry is not supported + globalThis.FinalizationRegistry = undefined; + const { JavaScript } = await (await import('./helper')).default; + const { Parser, Query } = await import('../src'); + const parser = new Parser(); + parser.setLanguage(JavaScript); + const tree = parser.parse('1+1')!; + const copyTree = tree.copy(); + const cursor = tree.walk(); + const copyCursor = cursor.copy(); + const lookaheadIterator = JavaScript.lookaheadIterator(cursor.currentNode.nextParseState)!; + const query = new Query(JavaScript, '(identifier) @element'); + parser.delete(); + tree.delete(); + copyTree.delete(); + cursor.delete(); + copyCursor.delete(); + lookaheadIterator.delete(); + query.delete(); + }); +});