cbm_arena_alloc(arena, 0) returns NULL, so the java and cs cross-registry
builders read a def_count of 0 as OOM and returned NULL — a corpus with no
files of that language silently lost its shared registry (and the seal tests
caught exactly that: cbm_cs_build_cross_registry(&arena, NULL, 0) == NULL).
Guard the partition alloc behind def_count > 0; the empty registry is still
built, finalized, and shared.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Cross-file LSP for Java rebuilt a type registry for EVERY file from a def
set the module filter reduces by a constant factor, not to a constant
size: the JVM filter branch includes every def sharing the file's
namespace, so per-file work tracks the corpus (measured defs_per_file
1,292 -> 5,031 in lockstep as defs_total went 179k -> 689k). That makes
the pass O(files x corpus_defs) — 87% of a Java index and the bulk of
the v0.9.0 -> v0.10.x slowdown.
Three changes, one architecture (the pattern Go/Python/C/C#/TS already
use):
1. cbm_java_build_cross_registry — the JVM def universe (Java + Kotlin,
for mixed source roots) built ONCE per run, sealed read-only, shared
across resolve workers. Wired into both pipeline.c and
pipeline_incremental.c.
2. Two-phase registration inside that build: all TYPES first, finalize
(hash buckets exist), then FUNCS. Func registration parses signatures,
and type-name qualification via cbm_registry_lookup_type is a LINEAR
scan until finalize — a single mixed pass measured 0.44 ms/def at
689k defs, a ~300 s sequential build that erased the sharing win.
Two-phase: 306 s -> 2.8 s. Stable partition order because overload
ties resolve to the first registered QN match.
3. cbm_run_java_lsp_cross_with_registry resolves each file against the
shared base through an overlay holding exactly THIS FILE's defs
(register_local_func_or_type_from_file). Own-file scope is the load-
bearing choice: patch_one_method refines signatures from the AST and
must write a private copy (its types live in the per-file arena, the
base is sealed), and any wider scope re-imports the quadratic — a
module/namespace-scoped overlay measured ratio 4.00 on the growing-
package corpus, own-file measures 2.00.
Elasticsearch corpus (46,477 files), same host, CBM_PROFILE=1:
v0.10.5 this change v0.9.0
wall 419.9 s 85.6 s (4.9x) 61.5 s
cross-LSP CPU 6,195,780ms 52,513ms (118x) 110,344 ms
us_per_file 207,448 1,770 3,722
us_per_file_per_kdef 300 2 —
nodes 693,100 693,100 —
edges 5,646,235 5,649,949 —
Cross-LSP CPU now beats v0.9.0. Nodes are identical; edges +0.066%,
consistent with the shared base resolving cross-package targets the old
per-file namespace/import filter could not see, plus source-order
independence from the two-phase build.
Guarded by the complexity suite's shared-package gate (RED at ratio 4.00
on the pre-change tree, 2.00 after — see the suite commit).
Refs #1669.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`import static demo.Util.twice` records the import target package-qualified
("demo.Util"), but the class is registered under the project/directory QN
("<proj>.Util") when the `package` declaration and the file's directory differ
(e.g. a flat layout). java_lookup_method("demo.Util", ...) then missed, the call
fell through to the lsp_static_import_text fallback, and the precise
lsp_static_import strategy was absent.
On a miss, resolve the import's class by its short name against the registry and
retry the method lookup, preferring an in-module match. Mirrors the C++
short-name type fallback.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A call through an interface-typed receiver (s.area() where s:Shape) hit
java_lookup_method on the interface's OWN abstract method and emitted
lsp_type_dispatch on Shape.area, never reaching the sole-implementer
resolution — so lsp_interface_resolve / lsp_interface_dispatch were never
produced.
Resolve interface dispatch first when the receiver is an interface: a sole
concrete in-project implementer -> lsp_interface_resolve (to that impl's
method, synthesizing the QN when the method registry lacks it); two-or-more
-> lsp_interface_dispatch; ZERO in-project impls -> fall through to
type_dispatch on the interface method, keeping JDK interface calls
(List/Stream/Predicate) strict.
The sole-impl detection is factored into java_find_sole_impl and hardened:
consult the method REGISTRY (the dispatch path's source of truth) since the
per-type method-name array is often empty, match supertypes by full QN OR
bare name (duplicate registry entries list a supertype short in one entry and
fully-qualified in another), and dedup implementers by QN so one class with
two registry entries is not miscounted as two implementers.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A constructor reference `ClassName::new` is extracted as a textual call to
`new`, but the LSP resolved it to a synthetic constructor QN with no graph
node, and the join keys on the callee short-name (`new` != the class/ctor
name), so both lsp_method_ref_ctor and lsp_method_ref_ctor_synth dropped
(callable=0).
Resolve the reference to the ClassName CLASS node (which exists, mirroring
the constructor-call handling) and stash the original textual callee `new`
in the resolved call's `reason` field; the join matches the textual `new`
call site on it, gated to the ctor-ref strategies. No struct/ABI change.
java_cs LSP reproductions 19->21.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Runtime-diagnosed (LSPDIAG2 at the pass_calls join) LSP-strategy reds.
NESTED-CLASS ENCLOSING QN (cbm_enclosing_func_qn, helpers.c): a method in a
nested class was qualified with only the INNERMOST class (Inner), not the full
chain (Outer.Inner) that the def walk's compute_class_qn produces. So a call
inside a nested-class method sourced to the file node (callable=0) AND its
enclosing_func_qn failed to equal the LSP-resolved caller QN (strategy ABSENT,
e.g. lsp_outer_dispatch). Build the full outer->inner dotted class chain so the
enclosing QN matches the class/method node QN. Single-class and top-level cases
are unchanged (the chain collapses to the prior value).
CONSTRUCTOR RESOLUTION (cs_lsp.c cs_resolve_object_creation, java_lsp.c): `new
Foo()` synthesized a callee `Foo..ctor` / `Foo.Foo` whose ".ctor"/doubled short
name joined nothing (call callee_name is "Foo") and pointed at a non-existent
node -> no edge (cs_ctor, cs_ctor_synthetic, lsp_constructor, lsp_constructor_synth
all dead). A C#/Java constructor registers as <type_qn>.<ClassShortName>, so look
it up by the class short name; when no explicit ctor exists, resolve the call to
the existing Foo CLASS node (short name "Foo" == callee_name -> join matches, node
exists -> edge forms carrying the strategy).
Reproduced by repro_lsp_java_cs (nested-dispatch + constructor strategies RED).
NOTE: the *_unindexed / lsp_unresolved strategies are intentionally below the
confidence floor and target missing symbols (no edge can exist) -- flagged
separately as likely test-scope issues, not fixed here.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Profiling symfony (416 s wall, one core pinned) convicted cbm_registry_lookup_type -> strcmp at 95% of samples: the PHP and Python def-registration loops probe the registry per Method (receiver-stub check) BEFORE the hash indexes exist, i.e. a linear scan over the growing type table - O(methods x types) per file, and the cross resolvers run per file. Java and Kotlin never called cbm_registry_finalize at all, leaving every lookup during their file walks linear over the whole cross registry. Fixes: (1) two-phase registration in php/py (types, finalize, then funcs/methods); (2) finalize added to the java/kotlin cross entry points; (3) lookups now tail-scan entries added after finalize - previously such entries were silently INVISIBLE to the hashed path (reproduce-first: tslsp_hash_registry_post_finalize_adds was red); (4) cbm_registry_finalize_into allocates the index from a per-call scratch arena - putting buckets into the pipeline-lifetime result arena per file accumulated +1.1 GB RSS on the FastAPI incremental test (incr_full_index caught it), and the py tier-2 builder (one def per call) skips mid-build finalize entirely. symfony: 416 s -> 12 s, identical node count; suite 5577/0.
bind_lambda_args and the method-reference arity path indexed the resolved method signature's NULL-terminated param_types array by the CALL-SITE argument index. A call with more arguments than the resolved overload declares (overload mismatch, varargs) read past the terminator and dereferenced whatever followed in the arena as a CBMType* - SIGSEGV while indexing elasticsearch (same out-of-bounds family as #427 and the bitcoin targs fix). Both sites now bound the index by the walked param count. Reproduce-first: 6-line fixture - run(Runnable) invoked with six lambdas - crashed rc=134 pre-fix; fork+exit-signal regression test lsp_java_lambda_args_exceed_params_no_crash added; suite 5574/0.
Indexing large OSS during the perf sweep crashed three LSP resolvers, all by stack overflow in their recursive resolve walks (macOS crash reports): elasticsearch — SIGSEGV in bind_lambda_args under deep recursive java_resolve_calls_in_node frames; bitcoin — SIGSEGV in cbm_type_substitute via c_adl_resolve under deep c_resolve_calls_in_node frames; microsoft/TypeScript — SIGBUS under an unbounded lookup_member_type cycle (cyclic type graph in reallyLargeFile.ts). The expression-eval guards (eval_depth) did not cover these walks. Each resolver now has a depth-guarded entry (walk_depth cap 512 for the Java/C AST walks, member_depth cap 64 for TS member lookup); past the cap the subtree resolves as unknown — graceful degradation instead of a crash. Reproduce-first: prod CLI probes crashed pre-fix (nested-call Java rc=139, nested-call C++ rc=139); fork+exit-signal regression tests added (TS cyclic shape needs a real cross-file registry, so it is verified at the real-repo tier — microsoft/TypeScript now indexes clean). Side-finding documented in the sweep report: Java BLOCK nesting >=3000 is minutes-slow (separate pathology, not this fix).
Remove leftover getenv(CBM_DBG_*) debug fprintf blocks in extract_calls.c/pass_calls.c (cppcheck always-true). Convert parse_lisp_imports to a stack-based walk (no recursion → no NOLINT, avoids deep-nesting overflow). Apply Homebrew clang-format to all touched sources. Local suite 5562/0, lint-ci clean.
Integrates the type-aware Java semantic resolver from
worktree-adding-java-lsp-support (based on 673ac4e): java_lsp.c + generated
java.lang/util stdlib registry, wired into the per-file LSP pass, with 94
java_lsp tests + ~170 java_lsp_coverage tests.
Wiring conflicts (the shared files diverged 146 commits on main) resolved by
keeping main's existing LSP registrations (go/c/php/py/ts/cs) and adding the
Java entries in lsp_all.c, cbm.c (cbm_run_java_lsp dispatch), test_main.c and
Makefile.cbm.
Adapted to the current unity-build: renamed java_lsp.c's static helpers
node_text/return_type_of to java_node_text/java_return_type_of so they no
longer collide with ts_lsp.c inside the shared lsp_all.c translation unit.
4082 tests pass (incl. java_lsp + java_lsp_coverage); ASan/UBSan clean.