perf(extract): answer previous siblings from the syntax index

ts_node_prev_sibling and ts_node_prev_named_sibling walk from the parent's first child to the node, so asking once per child of a wide parent is quadratic in sibling steps alone, and a walk back is worse. Sites that do it per definition or per statement include the decorators before a method, a Dart function_body's signature, a C `TEST(x) {}` body and a Dart selector's callee.

Both now route through the index, like ts_node_parent and ts_node_child, so every file that includes syntax_index.h gets them, including a call added later. The answer comes from the list the index already holds. A zero-width node keeps tree-sitter's own answer: its place among byte offsets is what ts_node__prev_sibling decides, and the list cannot.

complexity_syntax_index_prev_siblings_answer_as_tree_sitter_does compares both calls with tree-sitter on every node of eight sources, parse errors and MISSING tokens included. complexity_prev_sibling_walk_is_linear: plain 4.00, indexed 2.00.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-03 01:27:09 +01:00
1 parent 2a8771d3fe
commit ef19c95c0c
3 files changed
+252

No files matched your search

+52
View File
@@ -39,6 +39,22 @@ static void test_charge_parent(TSNode node) {
cbm_resolve_stats_add(CBM_RESOLVE_STAT_SYNTAX_CHILD_STEPS, steps);
}
}
/* The steps ts_node_prev_sibling takes: the children before node, under its parent. */
static void test_charge_siblings_before(TSNode node) {
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent))
return;
uint64_t steps = 0;
TSTreeCursor cursor = ts_tree_cursor_new(parent);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
steps++;
} while (ts_tree_cursor_current_node(&cursor).id != node.id &&
ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
cbm_resolve_stats_add(CBM_RESOLVE_STAT_SYNTAX_CHILD_STEPS, steps);
}
#endif
void cbm_syntax_index_begin(cbm_syntax_index_t *index, const TSTree *tree) {
@@ -192,6 +208,42 @@ TSNode cbm_syntax_previous_child(TSNode node) {
return found ? before : (TSNode){0};
}
/* Whether the table can say what comes before node, and if so the entry that does. */
static bool prev_from_table(cbm_syntax_index_t *index, TSNode node, uint32_t *before) {
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
if (atomic_load(&test_plain)) {
if (index && node.tree == index->tree)
test_charge_siblings_before(node);
return false;
}
#endif
if (!node.id || ts_node_start_byte(node) == ts_node_end_byte(node))
return false;
uint32_t at = table_position(index, node);
if (at == SYNTAX_NO_PARENT)
return false;
*before = index->before[at];
return true;
}
TSNode cbm_syntax_prev_sibling(TSNode node) {
cbm_syntax_index_t *index = current_index;
uint32_t before;
if (!prev_from_table(index, node, &before))
return ts_node_prev_sibling(node);
return before == SYNTAX_NO_PARENT ? (TSNode){0} : index->nodes[before];
}
TSNode cbm_syntax_prev_named_sibling(TSNode node) {
cbm_syntax_index_t *index = current_index;
uint32_t before;
if (!prev_from_table(index, node, &before))
return ts_node_prev_named_sibling(node);
while (before != SYNTAX_NO_PARENT && !ts_node_is_named(index->nodes[before]))
before = index->before[before];
return before == SYNTAX_NO_PARENT ? (TSNode){0} : index->nodes[before];
}
static cbm_syntax_wide_t *wide_slot(const cbm_syntax_index_t *index, const void *id) {
size_t mask = index->wide_capacity - 1;
size_t at = id_hash(id) & mask;
+7
View File
@@ -46,6 +46,11 @@ TSNode cbm_syntax_named_child(TSNode node, uint32_t position);
* for the first child and the root. Tree-sitter has no such call: code that wants
* it counts up to node from the first child, once per child of a wide parent. */
TSNode cbm_syntax_previous_child(TSNode node);
/* ts_node_prev_sibling and ts_node_prev_named_sibling walk from the parent's first child to
* node, so a walk back over a wide parent is quadratic in steps alone. A zero-width node has no
* place of its own among byte offsets, so tree-sitter's answer stands for it. */
TSNode cbm_syntax_prev_sibling(TSNode node);
TSNode cbm_syntax_prev_named_sibling(TSNode node);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* The plain calls, with the sibling steps they really take added to
@@ -58,6 +63,8 @@ void cbm_syntax_index_test_set_plain(bool plain);
#define ts_node_parent(node) cbm_syntax_parent(node)
#define ts_node_child(node, position) cbm_syntax_child(node, position)
#define ts_node_named_child(node, position) cbm_syntax_named_child(node, position)
#define ts_node_prev_sibling(node) cbm_syntax_prev_sibling(node)
#define ts_node_prev_named_sibling(node) cbm_syntax_prev_named_sibling(node)
#endif
#endif
+193
View File
@@ -67,6 +67,7 @@
#include "graph_buffer/graph_buffer.h"
#include "resolve/binding.h"
#include "resolve/rust_names.h"
#include "resolve/source_adapter.h"
#include "lsp/lsp_work.h"
#include "lsp/lsp_name_rows.h"
#define CBM_SYNTAX_INDEX_IMPLEMENTATION /* the seam only: this file asks tree-sitter directly */
@@ -3143,7 +3144,199 @@ TEST(complexity_parse_memo_lasts_one_file) {
PASS();
}
/* ── A language adapter's two phases ────────────────────────────────
* Over files already extracted, so what is counted is what the adapter itself walks: a
* name's definitions in a file, a library's directives, a type's bases. */
#define CX_LINE(fn, format) \
static const char *fn(char *out, size_t cap, int i) { \
snprintf(out, cap, format, i); \
return out; \
}
#define CX_LINE3(fn, format) \
static const char *fn(char *out, size_t cap, int i) { \
snprintf(out, cap, format, i, i, i); \
return out; \
}
typedef struct {
char path[64];
char *text;
} CxSourceFile;
static char *cx_lines_text(const char *head, CxLineFn line, const char *tail, int lines) {
size_t room = strlen(head) + strlen(tail) + (size_t)lines * CX_BINDING_LINE_ROOM + 1;
char *source = malloc(room);
if (!source)
return NULL;
size_t length = (size_t)snprintf(source, room, "%s", head);
for (int i = 0; i < lines; i++) {
char row[CX_BINDING_LINE_ROOM];
length += (size_t)snprintf(source + length, room - length, "%s", line(row, sizeof(row), i));
}
(void)snprintf(source + length, room - length, "%s", tail);
return source;
}
static uint64_t cx_source_work(CBMLanguage language, const CxSourceFile *files, int count) {
CBMFileResult **results = calloc((size_t)count, sizeof(*results));
cbm_source_table_t *table = cbm_source_table_new();
bool ok = results && table;
for (int f = 0; ok && f < count; f++)
results[f] = cbm_extract_file(files[f].text, (int)strlen(files[f].text), language, "test",
files[f].path, 0, NULL, NULL);
cbm_resolve_stats_t stats;
cbm_resolve_stats_reset(&stats);
cbm_resolve_stats_t *previous = cbm_resolve_stats_bind(&stats);
for (int f = 0; ok && f < count; f++)
ok = results[f] && cbm_source_collect(table, results[f], language, files[f].path,
files[f].text, strlen(files[f].text));
for (int f = 0; ok && f < count; f++) {
CBMArena arena;
cbm_arena_init(&arena);
ok = cbm_source_unit_build(&arena, table, results[f], language, files[f].path,
files[f].text, strlen(files[f].text)) != NULL;
cbm_arena_destroy(&arena);
}
cbm_resolve_stats_bind(previous);
uint64_t work = ok ? cx_stat_work(&stats, CBM_RESOLVE_STAT_LOOKUP_PROBES) : 0;
for (int f = 0; results && f < count; f++)
if (results[f])
cbm_free_result(results[f]);
free(results);
if (table)
cbm_source_table_free(table);
return work;
}
/* One generated file, at CX_BINDING_LINES lines and at twice that. */
static bool cx_source_shape_is_near_linear(CBMLanguage language, const char *path, const char *head,
CxLineFn line, const char *tail) {
uint64_t work[2] = {0, 0};
for (int round = 0; round < 2; round++) {
CxSourceFile file = {.text = cx_lines_text(head, line, tail, CX_BINDING_LINES << round)};
snprintf(file.path, sizeof(file.path), "%s", path);
if (file.text)
work[round] = cx_source_work(language, &file, 1);
free(file.text);
}
double ratio = cx_ratio((double)work[1], (double)work[0]);
printf(" %s source probes %llu -> %llu (ratio %.2f)\n", path, (unsigned long long)work[0],
(unsigned long long)work[1], ratio);
return work[0] >= CX_BINDING_LINES && ratio >= CX_RATIO_LO && ratio <= CX_RATIO_HI;
}
/* Every node of sources that parse, that do not, and that hold zero-width MISSING tokens: the
* routed calls answer exactly as tree-sitter does. */
TEST(complexity_syntax_index_prev_siblings_answer_as_tree_sitter_does) {
static const struct {
CBMLanguage language;
const char *source;
} cases[] = {
{CBM_LANG_PYTHON, "def f(a, b=1):\n return [a, (b), {'k': a}]\n\nclass C:\n x = 1\n"},
{CBM_LANG_TYPESCRIPT,
"@D() class A { @G('/x') m(a: number) { return a; } /* c */ n() {} }\nexport { A };\n"},
{CBM_LANG_GO, "package p\nfunc f() {\n r := g()\n r.GET(\"/x\", h)\n // c\n if r != nil "
"{ return }\n}\n"},
{CBM_LANG_C, "int f(void { return 1 }\nstruct S { int a };\nint g(void) { x = ; }\n"},
{CBM_LANG_DART, "int f() { return 1; }\nclass C { void m() {} }\nint g( { }\n"},
{CBM_LANG_JAVASCRIPT, "class { m( }\nfunction f(a { return a\n"},
{CBM_LANG_NASM, "global s\ns:\n mov eax, 1\n ret\n"},
{CBM_LANG_ELIXIR, "defmodule M do\n def f(a), do: a &&\nend\n"},
};
size_t visited = 0, zero_width = 0;
bool same = true;
for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) {
TSParser *parser = ts_parser_new();
ASSERT_NOT_NULL(parser);
ASSERT_TRUE(ts_parser_set_language(parser, cbm_ts_language(cases[c].language)));
TSTree *tree =
ts_parser_parse_string(parser, NULL, cases[c].source, (uint32_t)strlen(cases[c].source));
ts_parser_delete(parser);
ASSERT_NOT_NULL(tree);
cbm_syntax_index_t index;
cbm_syntax_index_begin(&index, tree);
TSTreeCursor cursor = ts_tree_cursor_new(ts_tree_root_node(tree));
for (bool more = true; more;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
visited++;
zero_width += ts_node_start_byte(node) == ts_node_end_byte(node);
same = same && ts_node_eq(cbm_syntax_prev_sibling(node), ts_node_prev_sibling(node));
same = same &&
ts_node_eq(cbm_syntax_prev_named_sibling(node), ts_node_prev_named_sibling(node));
if (ts_tree_cursor_goto_first_child(&cursor))
continue;
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cursor);
cbm_syntax_index_end(&index);
ts_tree_delete(tree);
}
ASSERT_TRUE(same);
ASSERT_GTE(visited, (size_t)100);
ASSERT_GTE(zero_width, (size_t)1);
PASS();
}
/* Asking for the sibling before each child of a wide parent, plainly and through the index. */
static uint64_t cx_prev_sibling_work(int lines, bool plain) {
char *source = cx_lines_text("", cx_line_py_statement, "", lines);
TSParser *parser = ts_parser_new();
TSTree *tree = NULL;
if (source && parser && ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_PYTHON)))
tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
if (parser)
ts_parser_delete(parser);
uint64_t work = 0;
if (tree) {
cbm_resolve_stats_t stats;
cbm_resolve_stats_reset(&stats);
cbm_resolve_stats_t *previous = cbm_resolve_stats_bind(&stats);
cbm_syntax_index_t index;
cbm_syntax_index_begin(&index, tree);
cbm_syntax_index_test_set_plain(plain);
TSTreeCursor cursor = ts_tree_cursor_new(ts_tree_root_node(tree));
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode child = ts_tree_cursor_current_node(&cursor);
(void)cbm_syntax_prev_named_sibling(child);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
cbm_syntax_index_test_set_plain(false);
cbm_syntax_index_end(&index);
cbm_resolve_stats_bind(previous);
work = cx_stat_work(&stats, CBM_RESOLVE_STAT_SYNTAX_CHILD_STEPS);
ts_tree_delete(tree);
}
free(source);
return work;
}
TEST(complexity_prev_sibling_walk_is_linear) {
uint64_t first = cx_prev_sibling_work(CX_BINDING_LINES, true);
uint64_t second = cx_prev_sibling_work(CX_BINDING_LINES * 2, true);
double plain = cx_ratio((double)second, (double)first);
printf(" plain prev sibling steps %llu -> %llu (ratio %.2f)\n", (unsigned long long)first,
(unsigned long long)second, plain);
ASSERT_GTE(first, (uint64_t)CX_BINDING_LINES);
ASSERT_TRUE(plain >= 3.2);
first = cx_prev_sibling_work(CX_BINDING_LINES, false);
second = cx_prev_sibling_work(CX_BINDING_LINES * 2, false);
double indexed = cx_ratio((double)second, (double)first);
printf(" indexed prev sibling steps %llu -> %llu (ratio %.2f)\n", (unsigned long long)first,
(unsigned long long)second, indexed);
ASSERT_GTE(first, (uint64_t)CX_BINDING_LINES);
ASSERT_TRUE(indexed >= CX_RATIO_LO && indexed <= CX_RATIO_HI);
PASS();
}
SUITE(complexity) {
RUN_TEST(complexity_syntax_index_prev_siblings_answer_as_tree_sitter_does);
RUN_TEST(complexity_prev_sibling_walk_is_linear);
RUN_TEST(complexity_import_map_is_linear_in_a_files_imports);
RUN_TEST(complexity_import_map_keeps_conflicts_and_repeats);
RUN_TEST(complexity_parse_memo_lasts_one_file);