From 52cda5f54101d8ee009decb4e486ac7fac9a1cbb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Sep 2019 20:54:21 -0700 Subject: [PATCH] Start work on wasm binding to query API --- lib/binding_web/binding.c | 41 +++++++++++ lib/binding_web/binding.js | 111 +++++++++++++++++++++++++---- lib/binding_web/exports.json | 7 ++ lib/binding_web/test/query-test.js | 51 +++++++++++++ 4 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 lib/binding_web/test/query-test.js diff --git a/lib/binding_web/binding.c b/lib/binding_web/binding.c index e6018b03..e94c5aa0 100644 --- a/lib/binding_web/binding.c +++ b/lib/binding_web/binding.c @@ -566,3 +566,44 @@ int ts_node_is_missing_wasm(const TSTree *tree) { TSNode node = unmarshal_node(tree); return ts_node_is_missing(node); } + +/******************/ +/* Section - Query */ +/******************/ + +void ts_query_exec_wasm( + const TSQuery *self, + TSQueryContext *context, + const TSTree *tree +) { + TSNode node = unmarshal_node(tree); + + Array(const void *) result = array_new(); + + unsigned index = 0; + unsigned match_count = 0; + ts_query_context_exec(context, node); + while (ts_query_context_next(context)) { + match_count++; + uint32_t pattern_index = ts_query_context_matched_pattern_index(context); + uint32_t capture_count; + const TSQueryCapture *captures = ts_query_context_matched_captures( + context, + &capture_count + ); + + array_grow_by(&result, 1 + 6 * capture_count); + + result.contents[index++] = (const void *)pattern_index; + result.contents[index++] = (const void *)capture_count; + for (unsigned i = 0; i < capture_count; i++) { + const TSQueryCapture *capture = &captures[i]; + result.contents[index++] = (const void *)capture->index; + marshal_node(result.contents + index, capture->node); + index += 5; + } + } + + TRANSFER_BUFFER[0] = (const void *)(match_count); + TRANSFER_BUFFER[1] = result.contents; +} diff --git a/lib/binding_web/binding.js b/lib/binding_web/binding.js index 4ce334cc..ac48cb70 100644 --- a/lib/binding_web/binding.js +++ b/lib/binding_web/binding.js @@ -5,6 +5,7 @@ const SIZE_OF_NODE = 5 * SIZE_OF_INT; const SIZE_OF_POINT = 2 * SIZE_OF_INT; const SIZE_OF_RANGE = 2 * SIZE_OF_INT + 2 * SIZE_OF_POINT; const ZERO_POINT = {row: 0, column: 0}; +const QUERY_WORD_REGEX = /[\w-.]*/; var VERSION; var MIN_COMPATIBLE_VERSION; @@ -143,9 +144,7 @@ class Parser { class Tree { constructor(internal, address, language, textCallback) { - if (internal !== INTERNAL) { - throw new Error('Illegal constructor') - } + assertInternal(internal); this[0] = address; this.language = language; this.textCallback = textCallback; @@ -201,9 +200,7 @@ class Tree { class Node { constructor(internal, tree) { - if (internal !== INTERNAL) { - throw new Error('Illegal constructor') - } + assertInternal(internal); this.tree = tree; } @@ -526,9 +523,7 @@ class Node { class TreeCursor { constructor(internal, tree) { - if (internal !== INTERNAL) { - throw new Error('Illegal constructor') - } + assertInternal(internal); this.tree = tree; unmarshalTreeCursor(this); } @@ -630,9 +625,7 @@ class TreeCursor { class Language { constructor(internal, address) { - if (internal !== INTERNAL) { - throw new Error('Illegal constructor') - } + assertInternal(internal); this[0] = address; this.types = new Array(C._ts_language_symbol_count(this[0])); for (let i = 0, n = this.types.length; i < n; i++) { @@ -672,6 +665,51 @@ class Language { return this.fields[fieldName] || null; } + query(source) { + const sourceLength = lengthBytesUTF8(source); + const sourceAddress = C._malloc(sourceLength + 1); + stringToUTF8(source, sourceAddress, sourceLength + 1); + const address = C._ts_query_new( + this[0], + sourceAddress, + sourceLength, + TRANSFER_BUFFER, + TRANSFER_BUFFER + SIZE_OF_INT + ); + if (address) { + const contextAddress = C._ts_query_context_new(address); + const captureCount = C._ts_query_capture_count(address); + const captureNames = new Array(captureCount); + for (let i = 0; i < captureCount; i++) { + const nameAddress = C._ts_query_capture_name_for_id( + address, + i, + TRANSFER_BUFFER + ); + const nameLength = getValue(TRANSFER_BUFFER, 'i32'); + captureNames[i] = UTF8ToString(nameAddress, nameLength); + } + return new Query(INTERNAL, address, contextAddress, captureNames); + } else { + const errorId = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32'); + const utf8ErrorOffset = getValue(TRANSFER_BUFFER, 'i32'); + const errorOffset = UTF8ToString(sourceAddress, utf8ErrorOffset).length; + C._free(sourceAddress); + const suffix = source.slice(errorOffset, 100); + switch (errorId) { + case 2: throw new RangeError( + `Bad node name '${suffix.match(QUERY_WORD_REGEX)[0]}'` + ); + case 3: throw new RangeError( + `Bad field name '${suffix.match(QUERY_WORD_REGEX)[0]}'` + ); + default: throw new SyntaxError( + `Bad syntax at offset ${errorOffset}: '${suffix}'...` + ); + } + } + } + static load(url) { let bytes; if ( @@ -704,6 +742,55 @@ class Language { } } +class Query { + constructor(internal, address, contextAddress, captureNames) { + assertInternal(internal); + this[0] = address; + this[1] = contextAddress; + this.captureNames = captureNames; + } + + delete() { + C._ts_query_delete(this[0]); + C._ts_query_context_delete(this[0]); + } + + exec(queryNode) { + marshalNode(queryNode); + + C._ts_query_exec_wasm(this[0], this[1], queryNode.tree[0]); + + const matchCount = getValue(TRANSFER_BUFFER, 'i32'); + const nodesAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32'); + const result = new Array(matchCount); + + let address = nodesAddress; + for (let i = 0; i < matchCount; i++) { + const pattern = getValue(address, 'i32'); + address += SIZE_OF_INT; + const captures = new Array(getValue(address, 'i32')); + address += SIZE_OF_INT; + for (let j = 0, n = captures.length; j < n; j++) { + const captureIndex = getValue(address, 'i32'); + address += SIZE_OF_INT; + const node = unmarshalNode(queryNode.tree, address); + address += SIZE_OF_NODE; + captures[j] = {name: this.captureNames[captureIndex], node}; + } + result[i] = {pattern, captures}; + } + + // Free the intermediate buffers + C._free(nodesAddress); + + return result; + } +} + +function assertInternal(x) { + if (x !== INTERNAL) throw new Error('Illegal constructor') +} + function isPoint(point) { return ( point && diff --git a/lib/binding_web/exports.json b/lib/binding_web/exports.json index a0cf9305..e2b187f7 100644 --- a/lib/binding_web/exports.json +++ b/lib/binding_web/exports.json @@ -68,6 +68,13 @@ "_ts_parser_new_wasm", "_ts_parser_parse_wasm", "_ts_parser_set_language", + "_ts_query_capture_count", + "_ts_query_capture_name_for_id", + "_ts_query_context_delete", + "_ts_query_context_new", + "_ts_query_delete", + "_ts_query_exec_wasm", + "_ts_query_new", "_ts_tree_cursor_current_field_id_wasm", "_ts_tree_cursor_current_node_id_wasm", "_ts_tree_cursor_current_node_is_missing_wasm", diff --git a/lib/binding_web/test/query-test.js b/lib/binding_web/test/query-test.js new file mode 100644 index 00000000..f02c5d86 --- /dev/null +++ b/lib/binding_web/test/query-test.js @@ -0,0 +1,51 @@ +const {assert} = require('chai'); +let Parser, JavaScript; + +describe("Query", () => { + let parser, tree, query; + + before(async () => + ({Parser, JavaScript} = await require('./helper')) + ); + + beforeEach(() => { + parser = new Parser().setLanguage(JavaScript); + }); + + afterEach(() => { + parser.delete(); + if (tree) tree.delete(); + if (query) query.delete(); + }); + + it('throws an error on invalid syntax', () => { + assert.throws(() => { + JavaScript.query("(function_declaration wat)") + }, "Bad syntax at offset 22: \'wat)\'..."); + assert.throws(() => { + JavaScript.query("(non_existent)") + }, "Bad node name 'non_existent'"); + assert.throws(() => { + JavaScript.query("(function_declaration non_existent:(identifier))") + }, "Bad field name 'non_existent'"); + }); + + it('matches simple queries', () => { + tree = parser.parse("function one() { two(); function three() {} }"); + const query = JavaScript.query(` + (function_declaration name:(identifier) @the-name) + `); + const matches = query.exec(tree.rootNode); + assert.deepEqual( + matches.map(({pattern, captures}) => ({ + pattern, + captures: captures.map(({name, node}) => ({name, text: node.text})) + })), + [ + {pattern: 0, captures: [{name: 'the-name', text: 'one'}]}, + // {pattern: 0, captures: [{name: 'the-function', text: 'two'}]}, + {pattern: 0, captures: [{name: 'the-name', text: 'three'}]}, + ] + ); + }); +});