Merge pull request #2702 from DeusData/fix/php-single-file-registry-finalize

fix(php): index the single-file LSP type registry
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-19 04:37:17 +02:00
commit 4527fc731a
4 files changed
+103

No files matched your search

+8
View File
@@ -17,8 +17,16 @@ extern _Thread_local uint64_t cbm_lsp_work_steps;
#define CBM_LSP_WORK(n) (cbm_lsp_work_steps += (uint64_t)(n))
/* Read and reset the calling thread's counter. */
uint64_t cbm_lsp_work_take(void);
/* Test-only work ceiling (0 = none). A regression test that reverts a bound
* sets it so the reverted, unbounded loop stops deterministically and the test
* reports RED instead of hanging the runner. Seam builds only: a product build
* has no ceiling, so it can never decide graph content. */
extern _Thread_local uint64_t cbm_lsp_work_ceiling;
#define CBM_LSP_WORK_CEILING_HIT() \
(cbm_lsp_work_ceiling != 0 && cbm_lsp_work_steps > cbm_lsp_work_ceiling)
#else
#define CBM_LSP_WORK(n) ((void)0)
#define CBM_LSP_WORK_CEILING_HIT() (0)
#endif
#endif /* CBM_LSP_WORK_H */
+17
View File
@@ -4372,6 +4372,17 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
}
}
/* Index the registry (F12). Unindexed, every lookup below was a linear
* scan over the stdlib plus every definition of the file: O(lookups x
* definitions), quadratic in the size of one file. First-registered
* answers keep the result of that scan (the first registration of a
* duplicate QN wins), so the output does not change. Index memory goes to
* a scratch arena that dies with this call, as in cbm_run_php_lsp_cross. */
reg.index_first_registered = true;
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
cbm_registry_finalize_into(&reg, &idx_arena);
/* Phase C: run the resolver. */
PHPLSPContext ctx;
php_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
@@ -4401,6 +4412,11 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
ctx.use_count = 0;
}
/* Trait flattening registered methods after the index was built; lookups
* would tail-scan them. Re-index once, first-registered as above, so the
* answers equal those of the indexed part followed by the tail scan. */
cbm_registry_finalize_into(&reg, &idx_arena);
php_lsp_process_file(&ctx, root);
if (ctx.debug) {
@@ -4414,6 +4430,7 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
rc->confidence);
}
}
cbm_arena_destroy(&idx_arena);
}
/* ── Cross-file + batch ───────────────────────────────────────── */
+1
View File
@@ -5,6 +5,7 @@
#ifdef CBM_ENABLE_TEST_SEAMS
_Thread_local uint64_t cbm_lsp_work_steps = 0;
_Thread_local uint64_t cbm_lsp_work_ceiling = 0;
uint64_t cbm_lsp_work_take(void) {
uint64_t v = cbm_lsp_work_steps;
cbm_lsp_work_steps = 0;
+77
View File
@@ -19,6 +19,9 @@
#include "cbm.h"
#include "../src/pipeline/lsp_resolve.h"
#include "lsp/php_lsp.h"
#include "lsp/lsp_work.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── Helpers ───────────────────────────────────────────────────── */
@@ -625,6 +628,79 @@ TEST(phplsp_class_aliased_trait_name_collision_terminates) {
PASS();
}
/* ── 23e. #765 / #1265: trait flattening work is counted, bounded, linear ──
*
* The guards above prove only that extraction returns; a regression would hang
* the runner instead of failing it. These tests count the LSP work
* (CBM_LSP_WORK, which flatten_trait_into_class adds to once per registry
* function it visits) under a test-only ceiling (lsp_work.h): if a flatten
* bound is reverted, the runaway loop stops at the ceiling and the test is RED
* deterministically. No clock is read (O9). */
enum { PHPLSP_FLATTEN_CEILING = 50000000 };
static uint64_t phplsp_counted_work(const char *src, int *ceiling_hit, CBMFileResult **out) {
(void)cbm_lsp_work_take();
cbm_lsp_work_ceiling = PHPLSP_FLATTEN_CEILING;
CBMFileResult *r = extract_php(src);
uint64_t work = cbm_lsp_work_take();
cbm_lsp_work_ceiling = 0;
*ceiling_hit = work > PHPLSP_FLATTEN_CEILING;
if (out) {
*out = r;
} else if (r) {
cbm_free_result(r);
}
return work;
}
/* F12: the single-file PHP LSP (cbm_run_php_lsp) never indexed its registry,
* so every type and method lookup was a linear scan over the stdlib plus every
* definition of the file. Each class here looks up the previous class and its
* method once: twice the classes must cost about twice the counted work; the
* linear scans drift toward four. */
static char *phplsp_chain_classes_source(int n) {
size_t sz = (size_t)n * 96 + 256;
char *src = malloc(sz);
if (!src) {
return NULL;
}
size_t pos = (size_t)snprintf(src, sz,
"<?php\nnamespace App\\Chain;\n\n"
"class C0 { public function m(): int { return 0; } }\n");
for (int i = 1; i < n; i++) {
pos += (size_t)snprintf(src + pos, sz - pos,
"class C%d { public function m(): int { return (new C%d())->m(); } }\n",
i, i - 1);
}
return src;
}
TEST(phplsp_single_file_lookup_work_is_linear_f12) {
enum { N1 = 1000, N2 = 2000 };
char *s1 = phplsp_chain_classes_source(N1);
char *s2 = phplsp_chain_classes_source(N2);
ASSERT_NOT_NULL(s1);
ASSERT_NOT_NULL(s2);
int hit1 = 0;
int hit2 = 0;
CBMFileResult *r2 = NULL;
uint64_t w1 = phplsp_counted_work(s1, &hit1, NULL);
uint64_t w2 = phplsp_counted_work(s2, &hit2, &r2);
free(s1);
free(s2);
printf(" chained classes %d -> %d: work %llu -> %llu (ratio %.2f)\n", N1, N2,
(unsigned long long)w1, (unsigned long long)w2, w1 ? (double)w2 / (double)w1 : 0.0);
ASSERT_NOT_NULL(r2);
ASSERT_FALSE(hit1);
ASSERT_FALSE(hit2);
ASSERT(w1 > 0);
/* The last class's call still resolves to the previous class's method. */
ASSERT(find_resolved(r2, "C1999.m", "C1998.m") >= 0);
cbm_free_result(r2);
ASSERT(w2 * 10 <= w1 * 25); /* ratio <= 2.5 */
PASS();
}
/* ── 24. Match expression result type ──────────────────────────── */
TEST(phplsp_match_result_type) {
@@ -5671,6 +5747,7 @@ SUITE(php_lsp) {
RUN_TEST(phplsp_trait_self_use_terminates);
RUN_TEST(phplsp_trait_aliased_import_name_collision_terminates);
RUN_TEST(phplsp_class_aliased_trait_name_collision_terminates);
RUN_TEST(phplsp_single_file_lookup_work_is_linear_f12);
RUN_TEST(phplsp_match_result_type);
RUN_TEST(phplsp_ternary_result_type);
RUN_TEST(phplsp_method_chain_depth_three);