From 3b823ccf2f99ad01a481f1f570f9eb12095ad102 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 3 Nov 2026 03:32:36 +0100 Subject: [PATCH] Lists a file or a class can outgrow grow; a file's size decides nothing Bases and mixins of Dart, PHP, Swift, Scala, Ruby and Perl classes, a PHP variable's writes, a Python function's returns, the files a shell process sources, the C# using-directive walk and the headers a C translation unit reaches were fixed arrays: past them a declaration was dropped or its class went open. They grow on the walk's arena now. A shell command asks the files that declare its name instead of every file of the process. The binding walks, the source adapters and the proof passes each pass over a file a bounded number of times, and their node and visit counts voided a large generated file whole. Only their nesting guards stay. A Rust trait's method names are no longer cut at 63. Tests: a class of 70 traits, a variable of 12 writes, a class of 40 bases, a process of 300 sourced files, the seen set's growth and depth guard, and the extension-named imports of item 4, which bind nothing. The chain test of item 1 now measures the walk alone, and the barrel's class is no substring of another needle. Signed-off-by: Martin Vogel --- internal/cbm/lsp/c_lsp.c | 44 ++-- internal/cbm/lsp/c_proof.c | 8 +- internal/cbm/lsp/cs_lsp.c | 33 ++- internal/cbm/lsp/cs_proof.c | 2 +- internal/cbm/lsp/kotlin_proof.c | 2 +- internal/cbm/lsp/lsp_all.c | 0 internal/cbm/lsp/oo_core.c | 6 +- internal/cbm/lsp/php_proof.c | 2 +- internal/cbm/lsp/py_proof.c | 2 +- internal/cbm/lsp/rust_lsp.c | 7 +- internal/cbm/lsp/rust_proof.c | 2 +- internal/cbm/resolve/adapter_dart.c | 24 +- internal/cbm/resolve/adapter_perl.c | 40 ++- internal/cbm/resolve/adapter_php.c | 52 ++-- internal/cbm/resolve/adapter_ruby.c | 29 ++- internal/cbm/resolve/adapter_scala.c | 18 +- internal/cbm/resolve/adapter_shell.c | 57 +++-- internal/cbm/resolve/adapter_swift.c | 18 +- internal/cbm/resolve/binding.c | 40 +-- internal/cbm/resolve/source_adapter.c | 28 ++- .../cbm/resolve/source_adapter_internal.h | 13 +- tests/test_complexity.c | 116 ++++++++- tests/test_resolver_source_langs.c | 227 +++++++++++++++++- 23 files changed, 576 insertions(+), 194 deletions(-) create mode 100644 internal/cbm/lsp/lsp_all.c diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 8b081d6dc..d12b12fcc 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -2160,26 +2160,38 @@ static bool c_proof_signature_equal(const CBMType *left, const CBMType *right) { static bool c_proof_file_reaches(CLSPContext *ctx, const char *from, const char *to) { if (!strcmp(from, to)) return true; - enum { REACH_LIMIT = 4096 }; - const char **seen = cbm_arena_alloc(ctx->arena, REACH_LIMIT * sizeof(*seen)); - if (!seen) + CBMHashTable *known = cbm_ht_create(64); + if (!known) return false; - int head = 0, tail = 0; - seen[tail++] = from; - while (head < tail) { - const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, seen[head++]); - for (int n = 0; facts && n < facts->include_count; n++) { + /* A file enters once; each entry lives on the arena, so the walk needs no second list. */ + typedef struct c_reach_file { + const char *file; + struct c_reach_file *next; + } c_reach_file_t; + c_reach_file_t *head = cbm_arena_alloc(ctx->arena, sizeof(*head)); + c_reach_file_t *tail = head; + if (head) + *head = (c_reach_file_t){.file = from}; + cbm_ht_set(known, from, (void *)from); + bool reached = false; + for (; head && !reached; head = head->next) { + const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, head->file); + for (int n = 0; facts && n < facts->include_count && !reached; n++) { const char *next = facts->includes[n]; - if (!strcmp(next, to)) - return true; - bool known = false; - for (int k = 0; k < tail && !known; k++) - known = !strcmp(seen[k], next); - if (!known && tail < REACH_LIMIT) - seen[tail++] = next; + reached = !strcmp(next, to); + if (reached || cbm_ht_get(known, next)) + continue; + c_reach_file_t *entry = cbm_arena_alloc(ctx->arena, sizeof(*entry)); + if (!entry) + continue; + *entry = (c_reach_file_t){.file = next}; + tail->next = entry; + tail = entry; + cbm_ht_set(known, next, (void *)next); } } - return false; + cbm_ht_free(known); + return reached; } static const char *c_proof_func_file(CLSPContext *ctx, const CBMRegisteredFunc *candidate, diff --git a/internal/cbm/lsp/c_proof.c b/internal/cbm/lsp/c_proof.c index a87b56212..9ce07bada 100644 --- a/internal/cbm/lsp/c_proof.c +++ b/internal/cbm/lsp/c_proof.c @@ -50,7 +50,7 @@ static const char *csp_text(csp_context_t *ctx, TSNode node) { : NULL; } static bool csp_budget(csp_context_t *ctx, unsigned depth) { - if (++ctx->visited > 1000000 || depth > 128) + if (depth > 128) ctx->failed = true; return !ctx->failed; } @@ -169,15 +169,11 @@ static bool csp_builtin_word(const char *text, uint32_t length) { bool cbm_c_preprocessor_domain_unsupported(TSNode root, const char *source) { TSTreeCursor cursor = ts_tree_cursor_new(root); - uint32_t visited = 0; bool unsupported = false; + /* One pass over the file, so its size decides nothing. */ for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); - if (++visited > 1000000) { - unsupported = true; - break; - } if (!strcmp(kind, "preproc_def") || !strcmp(kind, "preproc_function_def")) { TSNode name = ts_node_child_by_field_name(node, "name", 4); if (!ts_node_is_null(name) && diff --git a/internal/cbm/lsp/cs_lsp.c b/internal/cbm/lsp/cs_lsp.c index ac755cf2a..54f354383 100644 --- a/internal/cbm/lsp/cs_lsp.c +++ b/internal/cbm/lsp/cs_lsp.c @@ -2859,15 +2859,18 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { * whether it sits inside a namespace declaration: a block namespace's * body, or the compilation-unit siblings after a file-scoped namespace * (the grammar may attach those either way). */ - enum { CS_IMPORT_WALK_MAX = 256 }; - TSNode stack[CS_IMPORT_WALK_MAX]; - bool in_ns[CS_IMPORT_WALK_MAX]; - int top = 0; - in_ns[top] = false; - stack[top++] = root; + typedef struct { + TSNode node; + bool in_ns; + } cs_import_walk_t; + enum { CS_IMPORT_WALK_INLINE = 256 }; + cs_import_walk_t held[CS_IMPORT_WALK_INLINE]; + cs_import_walk_t *stack = held; + int top = 0, capacity = CS_IMPORT_WALK_INLINE; + stack[top++] = (cs_import_walk_t){.node = root, .in_ns = false}; while (top > 0) { - TSNode n = stack[--top]; - bool n_in_ns = in_ns[top]; + TSNode n = stack[--top].node; + bool n_in_ns = stack[top].in_ns; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); @@ -2967,7 +2970,7 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { bool child_in_ns = n_in_ns || strcmp(k, "namespace_declaration") == 0 || strcmp(k, "file_scoped_namespace_declaration") == 0; uint32_t cnc = ts_node_child_count(n); - for (uint32_t i = 0; i < cnc && top < CS_IMPORT_WALK_MAX; i++) { + for (uint32_t i = 0; i < cnc; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c)) continue; @@ -2976,8 +2979,16 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "block") == 0) { continue; } - in_ns[top] = child_in_ns; - stack[top++] = c; + if (top == capacity) { + cs_import_walk_t *grown = + cbm_arena_alloc(ctx->arena, (size_t)capacity * 2 * sizeof(*grown)); + if (!grown) + return; + memcpy(grown, stack, (size_t)top * sizeof(*grown)); + stack = grown; + capacity *= 2; + } + stack[top++] = (cs_import_walk_t){.node = c, .in_ns = child_in_ns}; /* A file-scoped namespace covers the siblings that follow it. */ if (strcmp(ck, "file_scoped_namespace_declaration") == 0) child_in_ns = true; diff --git a/internal/cbm/lsp/cs_proof.c b/internal/cbm/lsp/cs_proof.c index 2d5e58208..4f573f481 100644 --- a/internal/cbm/lsp/cs_proof.c +++ b/internal/cbm/lsp/cs_proof.c @@ -53,7 +53,7 @@ static const char *csp_text(cs_proof_source_t *source, TSNode node) { : NULL; } static bool csp_budget(cs_proof_source_t *source, unsigned depth) { - if (++source->visits > 1000000 || depth > 128) + if (depth > 128) source->failed = true; return !source->failed; } diff --git a/internal/cbm/lsp/kotlin_proof.c b/internal/cbm/lsp/kotlin_proof.c index 11532325f..da3501681 100644 --- a/internal/cbm/lsp/kotlin_proof.c +++ b/internal/cbm/lsp/kotlin_proof.c @@ -54,7 +54,7 @@ static const char *kt_proof_text(kt_proof_source_t *source, TSNode node) { : NULL; } static bool kt_proof_budget(kt_proof_source_t *source, unsigned depth) { - if (++source->visits > 1000000 || depth > 128) + if (depth > 128) source->failed = true; return !source->failed; } diff --git a/internal/cbm/lsp/lsp_all.c b/internal/cbm/lsp/lsp_all.c new file mode 100644 index 000000000..e69de29bb diff --git a/internal/cbm/lsp/oo_core.c b/internal/cbm/lsp/oo_core.c index b806525e2..5afb64bdb 100644 --- a/internal/cbm/lsp/oo_core.c +++ b/internal/cbm/lsp/oo_core.c @@ -2514,10 +2514,8 @@ void oo_walk_dispose(oo_walk_t *w) { } bool oo_budget(oo_walk_t *w) { - /* Each source node is visited a bounded number of times. Exhaustion - * discards the file's staged rows; it never publishes a partial walk. */ - if (++w->visits > 64u * 1024u * 1024u) - w->failed = true; + /* Each source node is visited a bounded number of times, so a file's size + * decides nothing. */ return !w->failed; } diff --git a/internal/cbm/lsp/php_proof.c b/internal/cbm/lsp/php_proof.c index 06af41f4a..1f65ce686 100644 --- a/internal/cbm/lsp/php_proof.c +++ b/internal/cbm/lsp/php_proof.c @@ -41,7 +41,7 @@ static const char *php_proof_text(php_source_t *source, TSNode node) { : NULL; } static bool php_proof_budget(php_source_t *source, unsigned depth) { - if (++source->visits > 1000000 || depth > 128) + if (depth > 128) source->failed = true; return !source->failed; } diff --git a/internal/cbm/lsp/py_proof.c b/internal/cbm/lsp/py_proof.c index 88d54ce02..eb02c11bc 100644 --- a/internal/cbm/lsp/py_proof.c +++ b/internal/cbm/lsp/py_proof.c @@ -47,7 +47,7 @@ static const char *pyp_text(pyp_source_t *source, TSNode node) { : NULL; } static bool pyp_budget(pyp_source_t *source, unsigned depth) { - if (++source->visits > 1000000 || depth > 128) { + if (depth > 128) { source->failed = true; source->exhausted = true; } diff --git a/internal/cbm/lsp/rust_lsp.c b/internal/cbm/lsp/rust_lsp.c index e275ad8b6..9588108d3 100644 --- a/internal/cbm/lsp/rust_lsp.c +++ b/internal/cbm/lsp/rust_lsp.c @@ -5775,10 +5775,11 @@ void cbm_rust_build_local_registry(CBMArena *arena, CBMTypeRegistry *reg, CBMFil if (ti >= 0) { reg->types[ti].is_interface = true; if (!ts_node_is_null(body)) { - const char *methods[64]; - int mc = 0; uint32_t bc = ts_node_named_child_count(body); - for (uint32_t j = 0; j < bc && mc < 63; j++) { + const char **methods = + (const char **)cbm_arena_alloc(arena, (bc + 1) * sizeof(const char *)); + int mc = 0; + for (uint32_t j = 0; methods && j < bc; j++) { TSNode item = ts_node_named_child(body, j); const char *ik = ts_node_type(item); if (strcmp(ik, "function_item") != 0 && diff --git a/internal/cbm/lsp/rust_proof.c b/internal/cbm/lsp/rust_proof.c index a88bd517a..5a2f60051 100644 --- a/internal/cbm/lsp/rust_proof.c +++ b/internal/cbm/lsp/rust_proof.c @@ -39,7 +39,7 @@ static const char *rsp_text(rsp_source_t *source, TSNode node) { : NULL; } static bool rsp_budget(rsp_source_t *source, unsigned depth) { - if (++source->visits > 1000000 || depth > 128) + if (depth > 128) source->failed = true; return !source->failed; } diff --git a/internal/cbm/resolve/adapter_dart.c b/internal/cbm/resolve/adapter_dart.c index a59423044..f0a145cca 100644 --- a/internal/cbm/resolve/adapter_dart.c +++ b/internal/cbm/resolve/adapter_dart.c @@ -460,9 +460,7 @@ static const char *dart_base_entry(cbm_source_walk_t *walk, char tag, TSNode typ /* Each base is recorded with its role, since the roles are searched in order. */ static void dart_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_decl_t *record) { - enum { MAX_BASES = 32 }; - const char *found[MAX_BASES]; - size_t used = 0; + cbm_source_names_t found = {0}; bool open = false; uint32_t count = ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) { @@ -474,8 +472,8 @@ static void dart_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_dec /* `mixin M on Base` and `extension E on Base` see Base's members as their own. */ if (cbm_source_is(child, "type_identifier")) { const char *entry = dart_base_entry(walk, 'S', child); - if (entry && used < MAX_BASES) - found[used++] = entry; + if (entry) + open |= !cbm_source_names_push(walk, &found, entry); continue; } if (!tag) @@ -490,21 +488,17 @@ static void dart_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_dec if (!cbm_source_is(type, "type_identifier")) continue; const char *entry = dart_base_entry(walk, nested ? 'M' : tag, type); - if (!entry) - continue; - if (used == MAX_BASES) - open = true; - else - found[used++] = entry; + if (entry) + open |= !cbm_source_names_push(walk, &found, entry); } } } - const char **copy = cbm_source_table_alloc(walk, (used + 1) * sizeof(*copy)); + const char **copy = cbm_source_table_alloc(walk, (found.count + 1) * sizeof(*copy)); if (!copy) return; - for (size_t i = 0; i < used; i++) - copy[i] = found[i] ? cbm_source_table_intern(walk, found[i]) : NULL; - copy[used] = NULL; + for (size_t i = 0; i < found.count; i++) + copy[i] = found.items[i] ? cbm_source_table_intern(walk, found.items[i]) : NULL; + copy[found.count] = NULL; record->bases = copy; if (open) record->flags |= CBM_SOURCE_DECL_F_OPEN; diff --git a/internal/cbm/resolve/adapter_perl.c b/internal/cbm/resolve/adapter_perl.c index 6f97c3a61..a40b57035 100644 --- a/internal/cbm/resolve/adapter_perl.c +++ b/internal/cbm/resolve/adapter_perl.c @@ -42,7 +42,6 @@ #include #include "syntax_index.h" -enum { PERL_MAX_BASES = 32 }; #define PERL_EXPORTER_KEY "&exporter" #define PERL_EXPORT_PREFIX "&export:" @@ -165,16 +164,9 @@ static bool perl_literal_words(perl_ctx_t *ctx, TSNode node, perl_word_fn fn, vo return false; } -typedef struct { - const char *words[PERL_MAX_BASES]; - size_t count; -} perl_words_t; - static void perl_gather_word(perl_ctx_t *ctx, const char *word, void *user) { - (void)ctx; - perl_words_t *list = user; - if (list->count < PERL_MAX_BASES) - list->words[list->count++] = word; + if (!cbm_source_names_push(ctx->walk, user, word)) + cbm_source_fail(ctx->walk, "perl_word_allocation"); } static TSNode perl_use_arguments(TSNode use) { @@ -241,29 +233,29 @@ static void perl_collect_use(perl_ctx_t *ctx, TSNode use) { (void)perl_literal_words(ctx, arguments, perl_declare_imported, (void *)module); return; } - perl_words_t words = {0}; + cbm_source_names_t words = {0}; bool literal = ts_node_is_null(arguments) || perl_literal_words(ctx, arguments, perl_gather_word, &words); if (mojo) { /* -base means the Mojo::Base class itself, -strict and -signatures add no * parent, and -role composes methods this walk does not model. */ - perl_words_t parents = {0}; + cbm_source_names_t parents = {0}; bool open = !literal; for (size_t i = 0; i < words.count; i++) { - if (!strcmp(words.words[i], "-base")) + if (!strcmp(words.items[i], "-base")) perl_gather_word(ctx, "Mojo::Base", &parents); - else if (!strcmp(words.words[i], "-role")) + else if (!strcmp(words.items[i], "-role")) open = true; - else if (words.words[i][0] != '-') - perl_gather_word(ctx, words.words[i], &parents); + else if (words.items[i][0] != '-') + perl_gather_word(ctx, words.items[i], &parents); } - perl_set_bases(ctx, ctx->package, parents.words, parents.count, line, open); + perl_set_bases(ctx, ctx->package, parents.items, parents.count, line, open); return; } for (size_t i = 0; i < words.count; i++) - if (!strcmp(words.words[i], "Exporter")) + if (!strcmp(words.items[i], "Exporter")) perl_note_exporter(ctx, ctx->package, line); - perl_set_bases(ctx, ctx->package, words.words, words.count, line, !literal); + perl_set_bases(ctx, ctx->package, words.items, words.count, line, !literal); } // cppcheck-suppress constParameterCallback @@ -279,12 +271,12 @@ static void perl_collect_array(perl_ctx_t *ctx, const char *array, TSNode right, if (!array) return; if (!strcmp(array, "ISA")) { - perl_words_t words = {0}; + cbm_source_names_t words = {0}; bool literal = perl_literal_words(ctx, right, perl_gather_word, &words); for (size_t i = 0; i < words.count; i++) - if (!strcmp(words.words[i], "Exporter")) + if (!strcmp(words.items[i], "Exporter")) perl_note_exporter(ctx, ctx->package, line); - perl_set_bases(ctx, ctx->package, words.words, words.count, line, !literal); + perl_set_bases(ctx, ctx->package, words.items, words.count, line, !literal); } else if (!strcmp(array, "EXPORT") || !strcmp(array, "EXPORT_OK")) { (void)perl_literal_words(ctx, right, perl_declare_export, (void *)ctx->package); } @@ -416,13 +408,13 @@ static void perl_collect_node(perl_ctx_t *ctx, TSNode node, unsigned depth) { const char *array = cbm_source_text(ctx->walk, ts_node_named_child(arguments, 0)); if (array && !strcmp(array, "@ISA")) { - perl_words_t words = {0}; + cbm_source_names_t words = {0}; uint32_t count = ts_node_named_child_count(arguments); bool literal = true; for (uint32_t i = 1; i < count; i++) literal &= perl_literal_words(ctx, ts_node_named_child(arguments, i), perl_gather_word, &words); - perl_set_bases(ctx, ctx->package, words.words, words.count, + perl_set_bases(ctx, ctx->package, words.items, words.count, ts_node_start_point(child).row + 1, !literal); } } diff --git a/internal/cbm/resolve/adapter_php.c b/internal/cbm/resolve/adapter_php.c index 1c341a444..41ce5edbf 100644 --- a/internal/cbm/resolve/adapter_php.c +++ b/internal/cbm/resolve/adapter_php.c @@ -26,7 +26,7 @@ #include #include "syntax_index.h" -enum { PHP_MAX_WRITES = 8, PHP_MAX_EXPR_DEPTH = 24 }; +enum { PHP_MAX_EXPR_DEPTH = 24 }; typedef enum { PHP_USE_CLASS, PHP_USE_FUNCTION, PHP_USE_CONST } php_use_kind_t; @@ -52,8 +52,8 @@ typedef struct php_var { const char *type; /* proven canonical type */ bool param, blocked, computed, computing; uint32_t available_from, escaped_from; - TSNode rhs[PHP_MAX_WRITES]; - int writes; + TSNode *rhs; /* every write's value, in source order */ + int writes, write_capacity; struct php_var *next; } php_var_t; @@ -359,10 +359,13 @@ static void php_declare_method(php_ctx_t *ctx, const char *owner, TSNode method) } } +static void php_push_base(php_ctx_t *ctx, cbm_source_names_t *found, const char *entry) { + if (!cbm_source_names_push(ctx->walk, found, entry)) + cbm_source_fail(ctx->walk, "php_base_allocation"); +} + static const char *const *php_bases(php_ctx_t *ctx, TSNode node, const char **parent) { - enum { MAX_BASES = 64 }; - const char *found[MAX_BASES]; - size_t used = 0; + cbm_source_names_t found = {0}; *parent = NULL; uint32_t count = ts_node_named_child_count(node); bool interface = cbm_source_is(node, "interface_declaration"); @@ -373,7 +376,7 @@ static const char *const *php_bases(php_ctx_t *ctx, TSNode node, const char **pa if (!base && !implements) continue; uint32_t members = ts_node_named_child_count(child); - for (uint32_t m = 0; m < members && used < MAX_BASES; m++) { + for (uint32_t m = 0; m < members; m++) { TSNode member = ts_node_named_child(child, m); if (!cbm_source_is(member, "name") && !cbm_source_is(member, "qualified_name")) continue; @@ -384,8 +387,9 @@ static const char *const *php_bases(php_ctx_t *ctx, TSNode node, const char **pa bool is_parent = base && !interface; if (is_parent) *parent = canonical; - found[used++] = - cbm_arena_sprintf(ctx->walk->arena, "%c:%s", is_parent ? 'P' : 'I', canonical); + php_push_base( + ctx, &found, + cbm_arena_sprintf(ctx->walk->arena, "%c:%s", is_parent ? 'P' : 'I', canonical)); } } TSNode body = cbm_source_field(node, "body"); @@ -395,21 +399,21 @@ static const char *const *php_bases(php_ctx_t *ctx, TSNode node, const char **pa if (!cbm_source_is(child, "use_declaration")) continue; uint32_t members = ts_node_named_child_count(child); - for (uint32_t m = 0; m < members && used < MAX_BASES; m++) { + for (uint32_t m = 0; m < members; m++) { TSNode member = ts_node_named_child(child, m); if (!cbm_source_is(member, "name") && !cbm_source_is(member, "qualified_name")) continue; const char *canonical = php_class_name(ctx, cbm_source_text(ctx->walk, member)); if (canonical) - found[used++] = cbm_arena_sprintf(ctx->walk->arena, "T:%s", canonical); + php_push_base(ctx, &found, cbm_arena_sprintf(ctx->walk->arena, "T:%s", canonical)); } } - const char **copy = cbm_source_table_alloc(ctx->walk, (used + 1) * sizeof(*copy)); + const char **copy = cbm_source_table_alloc(ctx->walk, (found.count + 1) * sizeof(*copy)); if (!copy) return NULL; - for (size_t i = 0; i < used; i++) - copy[i] = cbm_source_table_intern(ctx->walk, found[i]); - copy[used] = NULL; + for (size_t i = 0; i < found.count; i++) + copy[i] = cbm_source_table_intern(ctx->walk, found.items[i]); + copy[found.count] = NULL; return copy; } @@ -785,11 +789,21 @@ static void php_audit_scope(php_ctx_t *ctx, php_scope_t *scope, TSNode node, uns const char *name = cbm_source_text(ctx->walk, left); php_var_t *var = name ? php_var(ctx, scope, name) : NULL; if (var) { - if (var->writes < PHP_MAX_WRITES) - var->rhs[var->writes] = right; + if (var->writes == var->write_capacity) { + int capacity = var->write_capacity ? var->write_capacity * 2 : 4; + TSNode *rhs = + cbm_arena_alloc(ctx->walk->arena, (size_t)capacity * sizeof(*rhs)); + if (rhs) { + if (var->writes) + memcpy(rhs, var->rhs, (size_t)var->writes * sizeof(*rhs)); + var->rhs = rhs; + var->write_capacity = capacity; + } + } + if (var->writes < var->write_capacity) + var->rhs[var->writes++] = right; else var->blocked = true; - var->writes++; uint32_t end = ts_node_end_byte(node); if (!var->available_from || end < var->available_from) var->available_from = end; @@ -950,7 +964,7 @@ static const char *php_var_class(php_ctx_t *ctx, php_scope_t *scope, const char var->computing = true; const char *type = var->param ? var->declared : NULL; bool consistent = !var->param || var->declared; - for (int w = 0; consistent && w < var->writes && w < PHP_MAX_WRITES; w++) { + for (int w = 0; consistent && w < var->writes; w++) { const char *written = php_expr_class(ctx, scope, var->rhs[w], ts_node_start_byte(var->rhs[w]), depth + 1); consistent = written && (!type || !strcmp(type, written)); diff --git a/internal/cbm/resolve/adapter_ruby.c b/internal/cbm/resolve/adapter_ruby.c index 709be03c1..2f8989ddb 100644 --- a/internal/cbm/resolve/adapter_ruby.c +++ b/internal/cbm/resolve/adapter_ruby.c @@ -53,7 +53,7 @@ #include #include "syntax_index.h" -enum { RUBY_MAX_NEST = 32, RUBY_MAX_BASES = 64 }; +enum { RUBY_MAX_NEST = 32 }; /* Declaration facts of this adapter, above the shared flags. */ enum { @@ -393,7 +393,7 @@ static void ruby_note_body_call(ruby_ctx_t *ctx, TSNode call, bool conditional) /* Each mixin keeps the lexical path that its constant is read under. */ static void ruby_gather_mixin(ruby_ctx_t *ctx, TSNode call, bool in_singleton_class, - const char **bases, size_t *used) { + cbm_source_names_t *bases) { const char *name = ruby_call_name(ctx->walk, call); char tag = 0; /* Inside `class << self`, include is what extend is outside it. */ @@ -407,15 +407,17 @@ static void ruby_gather_mixin(ruby_ctx_t *ctx, TSNode call, bool in_singleton_cl return; TSNode arguments = cbm_source_field(call, "arguments"); uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); - for (uint32_t i = 0; i < count && *used < RUBY_MAX_BASES; i++) { + for (uint32_t i = 0; i < count; i++) { TSNode argument = ts_node_named_child(arguments, i); const char *text = NULL; if (cbm_source_is(argument, "constant") || cbm_source_is(argument, "scope_resolution") || cbm_source_is(argument, "self")) text = cbm_source_text(ctx->walk, argument); /* A computed mixin is an unknown ancestor; the '?' keeps that visible. */ - bases[(*used)++] = + const char *entry = cbm_arena_sprintf(ctx->walk->arena, "%c:%s|%s", tag, ruby_cref(ctx), text ? text : "?"); + if (!entry || !cbm_source_names_push(ctx->walk, bases, entry)) + cbm_source_fail(ctx->walk, "ruby_base_allocation"); } } @@ -459,8 +461,7 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u if (spelled) qn = cbm_source_def_qn(ctx->walk, spelled, line); cbm_source_decl_t *record = cbm_source_declare(ctx->walk, owner, "", qn, flags, line); - const char *bases[RUBY_MAX_BASES]; - size_t used = 0; + cbm_source_names_t bases = {0}; TSNode superclass = cbm_source_field(node, "superclass"); if (!ts_node_is_null(superclass) && ts_node_named_child_count(superclass)) { TSNode parent = ts_node_named_child(superclass, 0); @@ -469,8 +470,10 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u ? cbm_source_text(ctx->walk, parent) : NULL; /* Read in the enclosing nest: the class is not in scope for its own header. */ - bases[used++] = + const char *entry = cbm_arena_sprintf(ctx->walk->arena, "S:%s|%s", ruby_cref(ctx), text ? text : "?"); + if (!entry || !cbm_source_names_push(ctx->walk, &bases, entry)) + cbm_source_fail(ctx->walk, "ruby_base_allocation"); } if (!ruby_push(ctx, owner)) { cbm_source_fail(ctx->walk, "ruby_nesting_limit"); @@ -487,7 +490,7 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u do { TSNode child = ts_tree_cursor_current_node(&cursor); if (cbm_source_is(child, "call")) { - ruby_gather_mixin(ctx, child, false, bases, &used); + ruby_gather_mixin(ctx, child, false, &bases); } else if (cbm_source_is(child, "singleton_class") && cbm_source_is(cbm_source_field(child, "value"), "self")) { TSNode inner = cbm_source_field(child, "body"); @@ -498,7 +501,7 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u do { TSNode statement = ts_tree_cursor_current_node(&walker); if (cbm_source_is(statement, "call")) - ruby_gather_mixin(ctx, statement, true, bases, &used); + ruby_gather_mixin(ctx, statement, true, &bases); } while (ts_tree_cursor_goto_next_sibling(&walker)); } ts_tree_cursor_delete(&walker); @@ -508,11 +511,11 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u ts_tree_cursor_delete(&cursor); } if (record) { - const char **copy = cbm_source_table_alloc(ctx->walk, (used + 1) * sizeof(*copy)); + const char **copy = cbm_source_table_alloc(ctx->walk, (bases.count + 1) * sizeof(*copy)); if (copy) { - for (size_t i = 0; i < used; i++) - copy[i] = cbm_source_table_intern(ctx->walk, bases[i]); - copy[used] = NULL; + for (size_t i = 0; i < bases.count; i++) + copy[i] = cbm_source_table_intern(ctx->walk, bases.items[i]); + copy[bases.count] = NULL; record->bases = copy; } } diff --git a/internal/cbm/resolve/adapter_scala.c b/internal/cbm/resolve/adapter_scala.c index 6fd331b4d..1295822b4 100644 --- a/internal/cbm/resolve/adapter_scala.c +++ b/internal/cbm/resolve/adapter_scala.c @@ -45,7 +45,7 @@ #define SCALA_TYPE_NAME "type:" #define SCALA_OBJECT_NAME "object:" -enum { SCALA_MAX_PACKAGES = 8, SCALA_MAX_BASES = 32 }; +enum { SCALA_MAX_PACKAGES = 8 }; typedef struct scala_local { const char *name; @@ -286,8 +286,7 @@ static void scala_collect_members(cbm_source_walk_t *walk, const char *package, static void scala_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_decl_t *record, const char *type_params) { - const char *found[SCALA_MAX_BASES]; - size_t used = 0; + cbm_source_names_t found = {0}; bool open = false; TSNode extend = cbm_source_field(node, "extend"); uint32_t count = ts_node_is_null(extend) ? 0 : ts_node_named_child_count(extend); @@ -296,17 +295,14 @@ static void scala_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_de if (cbm_source_is(child, "arguments")) continue; const char *text = scala_type_text(walk, child, type_params); - if (!text || used == SCALA_MAX_BASES) - open = true; - else - found[used++] = text; + open |= !text || !cbm_source_names_push(walk, &found, text); } - const char **copy = cbm_source_table_alloc(walk, (used + 1) * sizeof(*copy)); + const char **copy = cbm_source_table_alloc(walk, (found.count + 1) * sizeof(*copy)); if (!copy) return; - for (size_t i = 0; i < used; i++) - copy[i] = cbm_source_table_intern(walk, found[i]); - copy[used] = NULL; + for (size_t i = 0; i < found.count; i++) + copy[i] = cbm_source_table_intern(walk, found.items[i]); + copy[found.count] = NULL; record->bases = copy; if (open) record->flags |= CBM_SOURCE_DECL_F_OPEN; diff --git a/internal/cbm/resolve/adapter_shell.c b/internal/cbm/resolve/adapter_shell.c index 1df30cc33..d474eeeaa 100644 --- a/internal/cbm/resolve/adapter_shell.c +++ b/internal/cbm/resolve/adapter_shell.c @@ -24,14 +24,16 @@ #define SHELL_VARIABLES "V:" #define SHELL_LOCALS "W:" #define SHELL_FILES "P:" +#define SHELL_DECLARERS "D:" /* a row per function: its name, and the file declaring it */ -enum { SHELL_PATH_BUF = 1024, SHELL_CLOSURE_MAX = 256 }; +enum { SHELL_PATH_BUF = 1024 }; typedef struct { cbm_source_walk_t *walk; const char *functions, *sources, *unset, *variables, *locals; const char **closure; /* files this script's process consists of, itself first */ size_t closure_count; + cbm_source_chain_t closure_seen; unsigned function_depth; } shell_ctx_t; @@ -221,6 +223,9 @@ static void shell_collect_walk(shell_ctx_t *ctx, TSNode node, unsigned depth) { uint32_t line = ts_node_start_point(node).row + 1; (void)cbm_source_declare(walk, ctx->functions, name, cbm_source_def_qn_at(walk, name, line), 0, line); + cbm_source_decl_t *row = cbm_source_declare(walk, SHELL_DECLARERS, name, NULL, 0, line); + if (row) + row->type = cbm_source_table_intern(walk, walk->file); } } else if (cbm_source_is(node, "command")) { const char *word = shell_command_word(walk, node); @@ -273,33 +278,44 @@ static void shell_collect(cbm_source_walk_t *walk) { shell_collect_walk(&ctx, walk->root, 0); } -static bool shell_in_closure(const shell_ctx_t *ctx, const char *file) { - for (size_t i = 0; i < ctx->closure_count; i++) - if (!strcmp(ctx->closure[i], file)) - return true; - return false; +static bool shell_push_closure(shell_ctx_t *ctx, size_t *capacity, const char *file) { + if (ctx->closure_count == *capacity) { + size_t grown = *capacity ? *capacity * 2 : 16; + const char **items = cbm_arena_alloc(ctx->walk->arena, grown * sizeof(*items)); + if (!items) + return false; + if (ctx->closure_count) + memcpy(items, ctx->closure, ctx->closure_count * sizeof(*items)); + ctx->closure = items; + *capacity = grown; + } + ctx->closure[ctx->closure_count++] = file; + return true; } -/* Breadth first through literal `source` statements; each file enters once. Past the bound the - * process is not fully known, and a name no file so far declares stays unknown. */ +/* Breadth first through literal `source` statements; each file enters once. */ static bool shell_build_closure(shell_ctx_t *ctx) { cbm_source_walk_t *walk = ctx->walk; - ctx->closure = cbm_arena_calloc(walk->arena, SHELL_CLOSURE_MAX * sizeof(*ctx->closure)); - if (!ctx->closure) + cbm_source_chain_t *seen = &ctx->closure_seen; + size_t capacity = 0; + if (!cbm_source_chain_first_visit(walk, seen, walk->file) || + !shell_push_closure(ctx, &capacity, walk->file)) return false; - ctx->closure[ctx->closure_count++] = walk->file; for (size_t at = 0; at < ctx->closure_count; at++) { const char *owner = cbm_arena_sprintf(walk->arena, SHELL_SOURCES "%s", ctx->closure[at]); if (!owner) return false; for (const cbm_source_decl_t *entry = cbm_source_lookup(walk, owner, ""); entry; entry = entry->next) { - if (!entry->type || !cbm_source_lookup(walk, SHELL_FILES, entry->type) || - shell_in_closure(ctx, entry->type)) + if (!entry->type || !cbm_source_lookup(walk, SHELL_FILES, entry->type)) continue; - if (ctx->closure_count == SHELL_CLOSURE_MAX) - return true; - ctx->closure[ctx->closure_count++] = entry->type; + if (!cbm_source_chain_first_visit(walk, seen, entry->type)) { + if (seen->overflow) + return false; + continue; + } + if (!shell_push_closure(ctx, &capacity, entry->type)) + return false; } } return true; @@ -332,10 +348,13 @@ static void shell_visit_command(shell_ctx_t *ctx, TSNode command) { return; } const cbm_source_decl_t *found = NULL; - for (size_t i = 1; i < ctx->closure_count; i++) { - const char *owner = cbm_arena_sprintf(walk->arena, SHELL_FUNCTIONS "%s", ctx->closure[i]); + for (const cbm_source_decl_t *row = cbm_source_lookup(walk, SHELL_DECLARERS, word); row; + row = row->next) { + if (!strcmp(row->type, walk->file) || !cbm_source_chain_has(&ctx->closure_seen, row->type)) + continue; + const char *owner = cbm_arena_sprintf(walk->arena, SHELL_FUNCTIONS "%s", row->type); const cbm_source_decl_t *decl = owner ? cbm_source_lookup(walk, owner, word) : NULL; - if (!decl) + if (!decl || decl == found) continue; /* The later `source` wins at runtime, and order across files is not modelled. */ if (found && (!decl->qn || !found->qn || strcmp(decl->qn, found->qn))) { diff --git a/internal/cbm/resolve/adapter_swift.c b/internal/cbm/resolve/adapter_swift.c index 0025d8865..6dc36e0c3 100644 --- a/internal/cbm/resolve/adapter_swift.c +++ b/internal/cbm/resolve/adapter_swift.c @@ -56,7 +56,7 @@ #define SWIFT_TARGET_KEY "@cbm:raw:swift-target:" #define SWIFT_AMBIGUOUS_PREFIX "@cbm:" -enum { SWIFT_MAX_LABELS = 32, SWIFT_MAX_BASES = 32, SWIFT_MAX_INITIALIZER_CHAIN = 64 }; +enum { SWIFT_MAX_LABELS = 32, SWIFT_MAX_INITIALIZER_CHAIN = 64 }; /* A type record of a struct: with no initializer anywhere it has one for its stored properties. */ enum { SWIFT_RECORD_STRUCT = 1u << 16 }; /* A parameter whose written type is a function: a trailing closure goes to one of these first. @@ -599,8 +599,7 @@ static TSNode swift_body_of(TSNode node) { } static void swift_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_decl_t *record) { - const char *found[SWIFT_MAX_BASES]; - size_t used = 0; + cbm_source_names_t found = {0}; bool open = false; uint32_t count = ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) { @@ -608,17 +607,14 @@ static void swift_note_bases(cbm_source_walk_t *walk, TSNode node, cbm_source_de if (!cbm_source_is(child, "inheritance_specifier")) continue; const char *text = swift_type_text(walk, cbm_source_field(child, "inherits_from"), 0); - if (!text || used == SWIFT_MAX_BASES) - open = true; - else - found[used++] = text; + open |= !text || !cbm_source_names_push(walk, &found, text); } - const char **copy = cbm_source_table_alloc(walk, (used + 1) * sizeof(*copy)); + const char **copy = cbm_source_table_alloc(walk, (found.count + 1) * sizeof(*copy)); if (!copy) return; - for (size_t i = 0; i < used; i++) - copy[i] = cbm_source_table_intern(walk, found[i]); - copy[used] = NULL; + for (size_t i = 0; i < found.count; i++) + copy[i] = cbm_source_table_intern(walk, found.items[i]); + copy[found.count] = NULL; record->bases = copy; if (open) record->flags |= CBM_SOURCE_DECL_F_OPEN; diff --git a/internal/cbm/resolve/binding.c b/internal/cbm/resolve/binding.c index d0ab0f054..2b9972a97 100644 --- a/internal/cbm/resolve/binding.c +++ b/internal/cbm/resolve/binding.c @@ -8,7 +8,7 @@ #include #include "syntax_index.h" -enum { BINDING_MAX_DEPTH = 512, BINDING_MAX_NODES = 1000000, BINDING_INITIAL_CAP = 32 }; +enum { BINDING_MAX_DEPTH = 512, BINDING_INITIAL_CAP = 32 }; enum { BINDING_DECLARATION = 1, BINDING_IMPORT = 2, @@ -1606,8 +1606,9 @@ static bool python_block_always_exits(TSNode block, unsigned depth) { /* Every value this function returns, outside nested scopes. A bare return and * None say nothing about the class of the others. Returns false when one is not * an access path, or the function yields. */ -static bool python_gather_returns(cbm_resolve_binding_index_t *index, TSNode node, const char **out, - uint32_t *count, uint32_t room, unsigned depth) { +static bool python_gather_returns(cbm_resolve_binding_index_t *index, TSNode node, + const char ***out, uint32_t *count, uint32_t *room, + unsigned depth) { if (depth > 48) return false; for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { @@ -1624,9 +1625,19 @@ static bool python_gather_returns(cbm_resolve_binding_index_t *index, TSNode nod if (kind_is(value, "none")) continue; const char *chain = python_chain(index, value, 0); - if (!chain || *count >= room) + if (!chain) return false; - out[(*count)++] = chain; + if (*count == *room) { + uint32_t capacity = *room ? *room * 2 : 4; + const char **items = cbm_arena_alloc(index->arena, capacity * sizeof(*items)); + if (!items) + return false; + if (*count) + memcpy(items, *out, *count * sizeof(*items)); + *out = items; + *room = capacity; + } + (*out)[(*count)++] = chain; continue; } if (!python_gather_returns(index, child, out, count, room, depth + 1)) @@ -1638,9 +1649,8 @@ static bool python_gather_returns(cbm_resolve_binding_index_t *index, TSNode nod /* Called once the function's own scope exists: its locals resolve the returns. */ static void python_note_returns(cbm_resolve_binding_index_t *index, TSNode function, size_t member, uint32_t scope) { - enum { RETURNS_MAX = 16 }; - const char *chains[RETURNS_MAX]; - uint32_t count = 0; + const char **chains = NULL; + uint32_t count = 0, room = 0; /* `-> Self` and `def f(self: T) -> T` return the receiver, whichever class that is. */ TSNode declared_return = field(function, "return_type"); binding_scope_t *body_scope = scope < index->scope_count ? &index->scopes[scope] : NULL; @@ -1679,7 +1689,7 @@ static void python_note_returns(cbm_resolve_binding_index_t *index, TSNode funct !strcmp(name, "__class_getitem__")); if (member >= index->python_member_count || (!special && !ts_node_is_null(field(function, "return_type"))) || - !python_gather_returns(index, field(function, "body"), chains, &count, RETURNS_MAX, 0) || + !python_gather_returns(index, field(function, "body"), &chains, &count, &room, 0) || !count) return; const char **kept = cbm_arena_alloc(index->arena, (count + 1) * sizeof(*kept)); @@ -4059,10 +4069,9 @@ static void note_dart_import(cbm_resolve_binding_index_t *index, TSNode node) { cbm_resolve_source_import_t import = {0}; TSTreeCursor cursor = ts_tree_cursor_new(node); unsigned depth = 0; - uint32_t visited = 0; for (;;) { TSNode child = ts_tree_cursor_current_node(&cursor); - if (++visited > BINDING_MAX_NODES || depth >= BINDING_MAX_DEPTH) { + if (depth >= BINDING_MAX_DEPTH) { fail_index(index, CBM_RESOLVE_ANALYSIS_LIMITED, "dart_import_walk_limit"); break; } @@ -5507,13 +5516,11 @@ static void walk_tree(cbm_resolve_binding_index_t *index, TSNode root) { const char *kinds[BINDING_MAX_DEPTH] = {0}; TSTreeCursor cursor = ts_tree_cursor_new(root); unsigned depth = 0; - uint32_t visited = 0; for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); /* This walk is linear in the file. A node count would void the largest - * ECMAScript sources for their size alone; only nesting has a real bound. */ - if ((++visited > BINDING_MAX_NODES && !javascript_language(index->language)) || - depth >= BINDING_MAX_DEPTH) { + * sources for their size alone; only nesting has a real bound. */ + if (depth >= BINDING_MAX_DEPTH) { fail_index(index, CBM_RESOLVE_ANALYSIS_LIMITED, "binding_walk_limit"); break; } @@ -5716,11 +5723,10 @@ static void python_follow_write_aliases(cbm_resolve_binding_index_t *index) { static void python_namespace_effects(cbm_resolve_binding_index_t *index, TSNode root) { TSTreeCursor cursor = ts_tree_cursor_new(root); unsigned depth = 0; - size_t visited = 0; index->python_effects_complete = true; for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); - if (++visited > BINDING_MAX_NODES || depth > BINDING_MAX_DEPTH) { + if (depth > BINDING_MAX_DEPTH) { index->python_effects_complete = false; index->python_writes_unseen = true; break; diff --git a/internal/cbm/resolve/source_adapter.c b/internal/cbm/resolve/source_adapter.c index 1ef137dde..447ac4adb 100644 --- a/internal/cbm/resolve/source_adapter.c +++ b/internal/cbm/resolve/source_adapter.c @@ -8,7 +8,7 @@ #include "stats.h" #include "syntax_index.h" -enum { SOURCE_MAX_VISITS = 4000000, SOURCE_MAX_DEPTH = 400 }; +enum { SOURCE_MAX_DEPTH = 400 }; struct cbm_source_table { CBMArena arena; @@ -196,7 +196,7 @@ void cbm_source_fail(cbm_source_walk_t *walk, const char *reason) { bool cbm_source_step(cbm_source_walk_t *walk, unsigned depth) { if (walk->failure) return false; - if (++walk->visits > SOURCE_MAX_VISITS || depth > SOURCE_MAX_DEPTH) { + if (depth > SOURCE_MAX_DEPTH) { cbm_source_fail(walk, "source_adapter_walk_limit"); return false; } @@ -240,6 +240,15 @@ static bool chain_grow(cbm_source_walk_t *walk, cbm_source_chain_t *chain) { return true; } +bool cbm_source_chain_has(const cbm_source_chain_t *chain, const char *owner) { + if (chain->capacity) + return *chain_slot(chain->slots, chain->capacity, owner) != NULL; + for (size_t i = 0; i < chain->count; i++) + if (!strcmp(chain->seen[i], owner)) + return true; + return false; +} + bool cbm_source_chain_first_visit(cbm_source_walk_t *walk, cbm_source_chain_t *chain, const char *owner) { if (!chain->capacity) { @@ -264,6 +273,21 @@ bool cbm_source_chain_first_visit(cbm_source_walk_t *walk, cbm_source_chain_t *c return true; } +bool cbm_source_names_push(cbm_source_walk_t *walk, cbm_source_names_t *names, const char *name) { + if (names->count == names->capacity) { + size_t capacity = names->capacity ? names->capacity * 2 : 8; + const char **items = cbm_arena_alloc(walk->arena, capacity * sizeof(*items)); + if (!items) + return false; + if (names->count) + memcpy(items, names->items, names->count * sizeof(*items)); + names->items = items; + names->capacity = capacity; + } + names->items[names->count++] = name; + return true; +} + bool cbm_source_token(TSNode node, const char *token) { uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { diff --git a/internal/cbm/resolve/source_adapter_internal.h b/internal/cbm/resolve/source_adapter_internal.h index 37d840dc4..bb410eb8f 100644 --- a/internal/cbm/resolve/source_adapter_internal.h +++ b/internal/cbm/resolve/source_adapter_internal.h @@ -71,7 +71,6 @@ typedef struct cbm_source_walk { const cbm_source_table_t *table; /* phase two only */ cbm_source_unit_t *unit; /* phase two only */ TSNode root; - uint64_t visits; const char *failure; const char *top_owner; /* collect: the owner a language files its top-level names under */ CBMHashTable *def_index; /* lazily built, owned by the walk */ @@ -143,6 +142,9 @@ typedef struct { bool cbm_source_chain_first_visit(cbm_source_walk_t *walk, cbm_source_chain_t *chain, const char *owner); +/* Whether this walk has visited the name. */ +bool cbm_source_chain_has(const cbm_source_chain_t *chain, const char *owner); + /* A walker that recurses: first_visit, and one level deeper until the matching leave. */ static inline bool cbm_source_chain_enter(cbm_source_walk_t *walk, cbm_source_chain_t *chain, const char *owner) { @@ -159,6 +161,13 @@ static inline void cbm_source_chain_leave(cbm_source_chain_t *chain) { chain->depth--; } +/* A list that grows on the walk's arena, as a declaration's parent names are gathered. */ +typedef struct { + const char **items; + size_t count, capacity; +} cbm_source_names_t; +bool cbm_source_names_push(cbm_source_walk_t *walk, cbm_source_names_t *names, const char *name); + /* Syntax helpers shared by the language files; none of them decides anything. */ static inline bool cbm_source_is(TSNode node, const char *kind) { return !ts_node_is_null(node) && strcmp(ts_node_type(node), kind) == 0; @@ -172,7 +181,7 @@ static inline TSNode cbm_source_field(TSNode node, const char *name) { TSNode cbm_source_named_field(TSNode node, const char *name); const char *cbm_source_text(cbm_source_walk_t *walk, TSNode node); const char *cbm_source_lower(cbm_source_walk_t *walk, const char *text); -/* True while the file's walk stays inside its analysis budget. */ +/* True while the file's walk is not nested past what its stack can take. */ bool cbm_source_step(cbm_source_walk_t *walk, unsigned depth); void cbm_source_fail(cbm_source_walk_t *walk, const char *reason); bool cbm_source_token(TSNode node, const char *token); diff --git a/tests/test_complexity.c b/tests/test_complexity.c index 9d7e4072b..f6baff112 100644 --- a/tests/test_complexity.c +++ b/tests/test_complexity.c @@ -58,6 +58,7 @@ #include "lsp/rust_proof.h" #include "tree_sitter/api.h" #include "pipeline/pass_lsp_cross.h" +#include "resolve/source_adapter_internal.h" #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" #include "store/store.h" @@ -3520,7 +3521,7 @@ TEST(complexity_dart_barrel_exports_are_near_linear) { /* A type's ancestors, however many, are each read once. `deep` is a line of classes, one a level; * otherwise one class implements them all, which recurses one level. The member is nobody's, so * the walk reads them all. */ -static uint64_t cx_dart_ancestors_work(int ancestors, bool deep) { +static uint64_t cx_dart_ancestors_work(int ancestors, bool deep, bool asked) { size_t room = (size_t)ancestors * 64 + 256; char *text = malloc(room); if (!text) @@ -3532,13 +3533,14 @@ static uint64_t cx_dart_ancestors_work(int ancestors, bool deep) { i - 1) : (size_t)snprintf(text + used, room - used, "class C%d {}\n", i); if (deep) { - used += (size_t)snprintf(text + used, room - used, "void use(C%d c) { c.nobody(); }\n", - ancestors - 1); + used += (size_t)snprintf(text + used, room - used, "void use(C%d c) { %s }\n", + ancestors - 1, asked ? "c.nobody();" : ""); } else { used += (size_t)snprintf(text + used, room - used, "class All implements C0"); for (int i = 1; i < ancestors; i++) used += (size_t)snprintf(text + used, room - used, ", C%d", i); - used += (size_t)snprintf(text + used, room - used, " {}\nvoid use(All c) { c.nobody(); }\n"); + used += (size_t)snprintf(text + used, room - used, " {}\nvoid use(All c) { %s }\n", + asked ? "c.nobody();" : ""); } CxSourceFile file = {.text = text}; snprintf(file.path, sizeof(file.path), "lib/chain.dart"); @@ -3546,9 +3548,15 @@ static uint64_t cx_dart_ancestors_work(int ancestors, bool deep) { free(text); return work; } +/* What asking one member costs, apart from reading the file. */ +static uint64_t cx_dart_walk_work(int ancestors, bool deep) { + uint64_t asked = cx_dart_ancestors_work(ancestors, deep, true); + uint64_t silent = cx_dart_ancestors_work(ancestors, deep, false); + return asked > silent ? asked - silent : 0; +} static bool cx_dart_ancestors_are_near_linear(int ancestors, bool deep) { - uint64_t first = cx_dart_ancestors_work(ancestors, deep); - uint64_t second = cx_dart_ancestors_work(ancestors * 2, deep); + uint64_t first = cx_dart_walk_work(ancestors, deep); + uint64_t second = cx_dart_walk_work(ancestors * 2, deep); double ratio = cx_ratio((double)second, (double)first); printf(" dart %s ancestors %d: probes %llu -> %llu (ratio %.2f)\n", deep ? "deep" : "wide", ancestors, (unsigned long long)first, (unsigned long long)second, ratio); @@ -3562,6 +3570,100 @@ TEST(complexity_source_chain_walk_reads_every_ancestor_once) { PASS(); } +/* A process sourcing every library, and calling something of each. */ +static uint64_t cx_shell_sources_work(int sourced) { + CxSourceFile *files = calloc((size_t)sourced + 1, sizeof(*files)); + size_t room = (size_t)sourced * 48 + 64; + char *text = malloc(room); + if (!files || !text) { + free(files); + free(text); + return 0; + } + size_t used = 0; + bool ok = true; + for (int i = 0; ok && i < sourced; i++) { + used += (size_t)snprintf(text + used, room - used, ". ./s%d.sh\n", i); + snprintf(files[i].path, sizeof(files[i].path), "s%d.sh", i); + files[i].text = malloc(48); + ok = files[i].text != NULL; + if (ok) + snprintf(files[i].text, 48, "f%d() { :; }\n", i); + } + used += (size_t)snprintf(text + used, room - used, "run() {\n"); + for (int i = 0; ok && i < sourced; i++) + used += (size_t)snprintf(text + used, room - used, " f%d\n", i); + used += (size_t)snprintf(text + used, room - used, "}\n"); + snprintf(files[sourced].path, sizeof(files[sourced].path), "main.sh"); + files[sourced].text = text; + uint64_t work = ok ? cx_source_work(CBM_LANG_BASH, files, sourced + 1) : 0; + for (int i = 0; i <= sourced; i++) + free(files[i].text); + free(files); + return work; +} +TEST(complexity_shell_commands_of_a_big_process_are_near_linear) { + uint64_t first = cx_shell_sources_work(500); + uint64_t second = cx_shell_sources_work(1000); + double ratio = cx_ratio((double)second, (double)first); + printf(" shell closure commands: probes %llu -> %llu (ratio %.2f)\n", + (unsigned long long)first, (unsigned long long)second, ratio); + ASSERT_GTE(first, (uint64_t)500); + ASSERT_TRUE(ratio >= CX_RATIO_LO && ratio <= CX_RATIO_HI); + PASS(); +} + +/* The seen set itself: names enter once, and a long walk stays cheap per name. */ +static uint64_t cx_seen_set_work(int names, bool *held) { + CBMArena arena; + cbm_arena_init(&arena); + cbm_source_walk_t walk = {.arena = &arena}; + cbm_source_chain_t chain = {0}; + cbm_resolve_stats_t stats; + cbm_resolve_stats_reset(&stats); + cbm_resolve_stats_t *previous = cbm_resolve_stats_bind(&stats); + char **texts = malloc((size_t)names * sizeof(*texts)); + *held = texts != NULL; + for (int i = 0; *held && i < names; i++) { + texts[i] = cbm_arena_sprintf(&arena, "lib/deep/n%d.dart", i); + *held &= cbm_source_chain_first_visit(&walk, &chain, texts[i]); + *held &= cbm_source_chain_has(&chain, texts[i]); + } + for (int i = 0; *held && i < names; i++) + *held &= !cbm_source_chain_first_visit(&walk, &chain, texts[i]); + *held = *held && !chain.overflow && chain.count == (size_t)names; + cbm_resolve_stats_bind(previous); + uint64_t work = cx_stat_work(&stats, CBM_RESOLVE_STAT_LOOKUP_PROBES); + free(texts); + cbm_arena_destroy(&arena); + return work; +} +TEST(complexity_source_chain_seen_set_grows) { + bool first_held = false, second_held = false; + uint64_t first = cx_seen_set_work(5000, &first_held); + uint64_t second = cx_seen_set_work(10000, &second_held); + double ratio = cx_ratio((double)second, (double)first); + printf(" seen set: probes %llu -> %llu (ratio %.2f)\n", (unsigned long long)first, + (unsigned long long)second, ratio); + ASSERT_TRUE(first_held && second_held); + ASSERT_TRUE(ratio >= CX_RATIO_LO && ratio <= CX_RATIO_HI); + /* The depth guard: one name past it stops the walk and says so. */ + CBMArena arena; + cbm_arena_init(&arena); + cbm_source_walk_t walk = {.arena = &arena}; + cbm_source_chain_t chain = {0}; + bool entered = true; + for (int i = 0; i < CBM_SOURCE_CHAIN_DEPTH_MAX; i++) + entered &= cbm_source_chain_enter(&walk, &chain, cbm_arena_sprintf(&arena, "n%d", i)); + bool past = cbm_source_chain_enter(&walk, &chain, "past"); + bool flagged = chain.overflow; + cbm_arena_destroy(&arena); + ASSERT_TRUE(entered); + ASSERT_FALSE(past); + ASSERT_TRUE(flagged); + PASS(); +} + /* A function of route registrations asks, for each, what its receiver was bound to. */ CX_LINE(cx_line_go_route, " r.GET(\"/p%d\", h)\n") CX_LINE3(cx_line_go_group, " g%d := r.Group(\"/g%d\")\n g%d.GET(\"/x\", h)\n") @@ -3720,6 +3822,8 @@ SUITE(complexity) { RUN_TEST(complexity_swift_type_bases_are_near_linear); RUN_TEST(complexity_dart_barrel_exports_are_near_linear); RUN_TEST(complexity_source_chain_walk_reads_every_ancestor_once); + RUN_TEST(complexity_source_chain_seen_set_grows); + RUN_TEST(complexity_shell_commands_of_a_big_process_are_near_linear); RUN_TEST(complexity_go_route_bindings_are_near_linear); RUN_TEST(complexity_flat_preceding_label_is_linear); RUN_TEST(complexity_import_map_is_linear_in_a_files_imports); diff --git a/tests/test_resolver_source_langs.c b/tests/test_resolver_source_langs.c index 48cef41af..a61d9d010 100644 --- a/tests/test_resolver_source_langs.c +++ b/tests/test_resolver_source_langs.c @@ -6730,7 +6730,7 @@ TEST(resolver_source_dart_barrel_of_any_width_and_depth) { const char *extra = i == 150 ? "void hiddenFar() {}\n" : i == 151 ? "void shownFar() {}\nvoid notShownFar() {}\n" : i == 160 || i == 170 ? "void twice() {}\n" - : i == 190 ? "class Far {\n void go() {}\n}\n" + : i == 190 ? "class Wide {\n void go() {}\n}\n" : ""; sources[count++] = (RQ_Source){.path = sl_sprintf("lib/a%d.dart", i), .text = sl_sprintf("void f%d() {}\n%s", i, extra)}; @@ -6754,8 +6754,8 @@ TEST(resolver_source_dart_barrel_of_any_width_and_depth) { "void callsShown() { shownFar(); }\n" "void callsNotShown() { notShownFar(); }\n" "void callsTwice() { twice(); }\n" - "void callsClass() { Far(); }\n" - "void callsMember(Far far) { far.go(); }\n" + "void callsClass() { Wide(); }\n" + "void callsMember(Wide w) { w.go(); }\n" "void callsDeep() { deepName(); }\n" "void callsShadowed() { shadowed(); }\n" "void callsNobody() { nobody(); }\n")}; @@ -6768,10 +6768,10 @@ TEST(resolver_source_dart_barrel_of_any_width_and_depth) { "a199.f199", "Function", 1, "void f199() {}"), SL_CALL_AT("barrel-show-passes", "lib/main.dart", "main.callsShown", "shownFar()", 0, "lib/a151.dart", "a151.shownFar", "Function", 2, "void shownFar() {}"), - SL_CALL_AT("barrel-class", "lib/main.dart", "main.callsClass", "Far()", 0, "lib/a190.dart", - "a190.Far", "Class", 2, "class Far {"), - SL_CALL_AT("barrel-class-member", "lib/main.dart", "main.callsMember", "far.go", 0, - "lib/a190.dart", "Far.go", "Method", 3, " void go() {}"), + SL_CALL_AT("barrel-class", "lib/main.dart", "main.callsClass", "Wide()", 0, + "lib/a190.dart", "a190.Wide", "Class", 2, "class Wide {"), + SL_CALL_AT("barrel-class-member", "lib/main.dart", "main.callsMember", "w.go", 0, + "lib/a190.dart", "Wide.go", "Method", 3, " void go() {}"), SL_CALL_AT("chain-end", "lib/main.dart", "main.callsDeep", "deepName()", 0, "lib/deep199.dart", "deep199.deepName", "Function", 2, "void deepName() {}"), SL_CALL_AT("chain-shallower-hides-deeper", "lib/main.dart", "main.callsShadowed", @@ -6843,11 +6843,12 @@ TEST(resolver_source_dart_inheritance_of_any_depth) { TEST(resolver_source_php_inheritance_of_any_depth) { static const RQ_Expected expected[] = { - SL_CALL_AT("member-past-128-levels", "src/chain.php", "chain.useIt", "$c->rootM", 0, + SL_CALL_AT("member-past-128-levels", "src/chain.php", "chain.useIt", "$c->rootM()", 0, "src/chain.php", "C0.rootM", "Method", 3, " public function rootM() {}"), }; static const RQ_Forbidden forbidden[] = { - SL_DENY_AT("member-nobody-declares", "src/chain.php", "chain.useAbsent", "$c->neverM", 0), + SL_DENY_AT("member-nobody-declares", "src/chain.php", "chain.useAbsent", "$c->neverM()", + 0), }; char *text = sl_chain_text("run()", 0, + "src/writes.php", "A.run", "Method", 2, "class A { public function run() {} }"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("a-late-write-of-another-class", "src/writes.php", "writes.mixed", "$y->run()", + 0), + }; + char *writes = sl_repeat_text("", " $x = new A();\n", 12, ""); + char *other = sl_repeat_text("", " $y = new A();\n", 11, " $y = new B();\n"); + char *text = sl_sprintf("run();\n}\n" + "function mixed() {\n%s $y->run();\n}\n", + writes, other); + free(writes); + free(other); + RQ_Source sources[] = {{.path = "src/writes.php", .text = text}}; + bool ok = sl_check("php-writes", ".php", "inTrait69()", 0, + "src/traits.php", "T69.inTrait69", "Method", 71, + "trait T69 { public function inTrait69() {} }"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("nobody-declares-it", "src/traits.php", "Big.go", "$this->inNoTrait()", 0), + }; + char *traits = sl_repeat_text("inTrait69();\n" + " $this->inNoTrait();\n }\n}\n"); + char *text = sl_sprintf("%s%s", traits, uses); + free(traits); + free(uses); + RQ_Source sources[] = {{.path = "src/traits.php", .text = text}}; + bool ok = sl_check("php-traits", ".php", "