mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(lsp): TypeScript / JavaScript / JSX / TSX hybrid LSP resolver
Adds clean-room ts_lsp resolver covering single- and cross-file type resolution for TS, JS, JSX, TSX. Mirrors the structure and entry-point pattern of the existing Go and C/C++ LSP hybrids (single-file cbm_run_ts_lsp + batch cbm_run_ts_lsp_cross), with TS-specific surface: - Hash-table indexed type registry (FNV-1a, lazy via cbm_registry_finalize) - Partial structural relater for NAMED / BUILTIN / LITERAL / TUPLE / UNION / INTERSECTION / FUNC / TEMPLATE / TYPE_PARAM / ALIAS - Conditional types T extends U ? X : Y with union distribution - 'infer X' constraint solver, keyof / typeof / T[K] type-position parsing - Flow-sensitive narrowing (instanceof / typeof / discriminated unions), optional chaining, non-null assertion - TS utility-type passthrough (Partial, Pick, ReturnType, Awaited, etc.) - Function overload resolution by argument types - Async iterables + 'for await ... of' - Curated DOM stdlib subset + comprehensive base stdlib - Mode flags: js_mode (.js/.jsx with JSDoc inference), jsx_mode (JSX expressions), dts_mode (.d.ts ambient declarations, no resolved calls) Touches existing files only with additive changes: - type_rep.h/.c: new TS-specific CBMTypeKind values + constructors - type_registry.h/.c: TS-specific fields, hash indexes, cbm_registry_finalize - cbm.c: dispatch for JS/TS/TSX languages - lsp_all.c, Makefile.cbm, tests/test_main.c: include + suite registration - README.md: feature list mention 291 ts_lsp test cases. CBM_LSP_DISABLED=1 disables the resolver (matches CBM_LSP_DEBUG / CBM_LSP_DISABLED knobs in go_lsp / c_lsp).
This commit is contained in:
1 parent
673ac4ef8a
commit
d1143fbdce
14 files changed
+10903
-8
No files matched your search
+3
-1
@@ -324,6 +324,8 @@ TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c
|
||||
|
||||
TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
|
||||
|
||||
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
|
||||
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c
|
||||
|
||||
TEST_TRACES_SRCS = tests/test_traces.c
|
||||
@@ -343,7 +345,7 @@ TEST_SIMHASH_SRCS = tests/test_simhash.c
|
||||
|
||||
TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c
|
||||
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
|
||||
# ── Build directories ────────────────────────────────────────────
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
**The fastest and most efficient code intelligence engine for AI coding agents.** Full-indexes an average repository in milliseconds, the Linux kernel (28M LOC, 75K files) in 3 minutes. Answers structural queries in under 1ms. Ships as a single static binary for macOS, Linux, and Windows — download, run `install`, done.
|
||||
|
||||
High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across all 155 languages, enhanced with LSP-style hybrid type resolution for Go, C, and C++ (more languages coming soon) — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 14 MCP tools. Zero dependencies. Plug and play across 11 coding agents.
|
||||
High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across all 155 languages, enhanced with LSP-style hybrid type resolution for Go, C, C++, and TypeScript / JavaScript / JSX / TSX (more languages coming soon) — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 14 MCP tools. Zero dependencies. Plug and play across 11 coding agents.
|
||||
|
||||
> **Research** — The design and benchmarks behind this project are described in the preprint [*Codebase-Memory: Tree-Sitter-Based Knowledge Graphs for LLM Code Exploration via MCP*](https://arxiv.org/abs/2603.27277) (arXiv:2603.27277). Evaluated across 31 real-world repositories: 83% answer quality, 10× fewer tokens, 2.1× fewer tool calls vs. file-by-file exploration.
|
||||
|
||||
@@ -170,7 +170,7 @@ Removes all agent configs, skills, hooks, and instructions. Does not remove the
|
||||
- **155 vendored tree-sitter grammars** compiled into the binary
|
||||
- **Generic package / module resolution** — bare specifiers like `@myorg/pkg`, `github.com/foo/bar`, `use my_crate::foo` resolved via manifest scanning (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `composer.json`, `pubspec.yaml`, `pom.xml`, `build.gradle`, `mix.exs`, `*.gemspec`)
|
||||
- **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests, Kustomize overlays as graph nodes
|
||||
- **LSP-style hybrid type resolution** for Go, C, C++ (more languages coming)
|
||||
- **LSP-style hybrid type resolution** for Go, C, C++, and TypeScript / JavaScript / JSX / TSX (more languages coming) — clean-room reimplementation of tsserver / typescript-go's type resolution algorithms (parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files)
|
||||
- **RAM-first pipeline**: LZ4 compression, in-memory SQLite, single dump at end. Memory released after.
|
||||
|
||||
### Distribution & operation
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# Cross-File LSP Resolution Architecture
|
||||
|
||||
**Goal:** Wire LSP-augmented type-aware call resolution across files in the
|
||||
indexing pipeline, while keeping the latency overhead **under +10% of total
|
||||
indexing time** (vs the conservative +30% naive estimate).
|
||||
|
||||
This document is the design output of plan-task #1; implementation lands in
|
||||
plan-tasks #2 (hash-table registry) and #6 (pipeline wiring).
|
||||
|
||||
---
|
||||
|
||||
## 1. Current state (post-iter 37)
|
||||
|
||||
### Single-file flow (works today)
|
||||
|
||||
```
|
||||
cbm_extract_file(source, lang, project, rel_path, ...)
|
||||
├── tree-sitter parse → TSTree
|
||||
├── cbm_extract_definitions(ctx) // result->defs filled
|
||||
├── cbm_extract_imports(ctx)
|
||||
├── cbm_extract_unified(ctx) // tree-sitter call edges
|
||||
└── if lang in {TS, JS, TSX, JAVASCRIPT}:
|
||||
└── cbm_run_ts_lsp(arena, result, source, len, root, modes...)
|
||||
├── cbm_registry_init(reg) — local to this call
|
||||
├── cbm_ts_stdlib_register(reg)
|
||||
├── register_file_defs(reg, result->defs) // file-only
|
||||
├── ts_lsp_init(ctx, ...)
|
||||
├── ast_sweep_shapes(ctx, root, reg)
|
||||
├── rebuild_signatures_from_ast(ctx, root, reg)
|
||||
├── convert_signature_type_params(ctx, root, reg)
|
||||
├── apply_jsdoc_signatures(ctx, root, reg)
|
||||
├── infer_implicit_returns(ctx, root, reg)
|
||||
└── ts_lsp_process_file(ctx, root) // emits resolved_calls
|
||||
```
|
||||
|
||||
**Critical limitation:** `register_file_defs` only sees this file's defs.
|
||||
Cross-file types (`import { Foo } from './bar'` where `Foo` is defined in `bar.ts`)
|
||||
are NOT in the registry. So `c.method()` where `c: Foo` resolves to UNKNOWN
|
||||
because the resolver doesn't know about Foo's structure.
|
||||
|
||||
### Cross-file path (exists but pipeline-unwired)
|
||||
|
||||
```
|
||||
cbm_run_ts_lsp_cross(arena, source, ..., defs, def_count, imports, ..., out)
|
||||
```
|
||||
|
||||
Takes a manually-constructed `CBMLSPDef[]` array. The 4 cross-file tests
|
||||
build this array in C code. Pipeline doesn't call this.
|
||||
|
||||
### Latency profile today
|
||||
|
||||
For a 1M-LOC TS project (~3000 files):
|
||||
- Parse + extract: ~12 sec
|
||||
- Single-file LSP: ~3 sec
|
||||
- **Total: ~15 sec**
|
||||
|
||||
---
|
||||
|
||||
## 2. Naive cross-file approach (and why we reject it)
|
||||
|
||||
```
|
||||
Pass 1: extract every file (cbm_extract_file) [~12 sec]
|
||||
Pass 2: build project-wide def-map from all results [~0.5 sec]
|
||||
Pass 3: re-run LSP per file with project-wide map [~3 sec ?]
|
||||
— but each per-file LSP invocation re-parses, re-extracts shapes,
|
||||
re-builds the registry from scratch
|
||||
— 3000 files × ~5ms registry rebuild = ~15 sec ALONE
|
||||
— plus per-call O(n) lookups against the registry
|
||||
Total: ~30 sec (+100% over today)
|
||||
```
|
||||
|
||||
This is the +30% estimate I gave previously, and it's actually worse on closer
|
||||
inspection. Two specific waste points:
|
||||
|
||||
1. **Registry rebuilt N times** — every file builds its own
|
||||
`CBMTypeRegistry`, even though most contents are project-wide stdlib +
|
||||
shared cross-file defs.
|
||||
2. **Linear-scan registry lookups** — `cbm_registry_lookup_method` is O(n) on
|
||||
the func array. With ~5000 stdlib funcs (post-generator) + ~10000 user funcs
|
||||
(1M-LOC project), every call resolution is ~15000 strcmps. ~5M call
|
||||
resolutions × ~15us = ~75 sec **just on lookups**.
|
||||
|
||||
So the naive approach would actually be a 5–10× regression on real projects.
|
||||
|
||||
---
|
||||
|
||||
## 3. Target architecture (this design)
|
||||
|
||||
### Three-pass model with shared registry
|
||||
|
||||
```
|
||||
Pass 1: cbm_pipeline_pass_definitions [unchanged, ~12 sec]
|
||||
↓ each file's CBMFileResult contains its defs[]
|
||||
|
||||
Pass 1.5 (NEW): build project-wide registry [~0.5 sec]
|
||||
- Walk all files' result->defs
|
||||
- Insert into single hash-table-indexed CBMTypeRegistry
|
||||
- Stdlib registered once at the start (not per-file)
|
||||
|
||||
Pass 2: cbm_pipeline_pass_calls [unchanged tree-sitter calls]
|
||||
|
||||
Pass 2.5 (NEW): cbm_pipeline_pass_lsp_resolve [~2 sec target]
|
||||
- Per file (parallel via worker pool):
|
||||
* Reuse cached_tree (no re-parse)
|
||||
* Reuse cached defs from pass 1
|
||||
* Open a TSLSPContext pointing at the SHARED project-wide registry
|
||||
* Run ts_lsp_process_file()
|
||||
* Emit resolved_calls into the file's CBMFileResult
|
||||
- No registry rebuild per file. No re-extract. Just walk + resolve.
|
||||
|
||||
Pass 3+: existing usage/semantic/etc. [unchanged]
|
||||
```
|
||||
|
||||
### Key invariants
|
||||
|
||||
1. **One registry per project, not per file.** Built once after pass 1
|
||||
completes; lives for the duration of the indexing run.
|
||||
2. **Hash-table-indexed.** Adding a stdlib of 5000 funcs costs the same
|
||||
per-lookup as adding 50.
|
||||
3. **Cached AST trees reused.** `cbm_run_ts_lsp` already reads
|
||||
`result->cached_tree`; cross-file pass does the same.
|
||||
4. **Per-file resolution is independent.** Files can resolve in parallel
|
||||
via the existing `cbm_worker_pool_dispatch` mechanism.
|
||||
5. **Read-only registry during resolution.** All writes happen in pass 1.5;
|
||||
pass 2.5 readers don't lock.
|
||||
|
||||
---
|
||||
|
||||
## 4. Hash-table registry (task #2)
|
||||
|
||||
### Why O(1) is essential
|
||||
|
||||
| Item | Per-lookup cost (linear scan) | Per-lookup cost (hash) |
|
||||
|---|---|---|
|
||||
| Today's stdlib (~150 funcs) | ~1 µs | ~50 ns |
|
||||
| Project funcs (~10K user funcs) | ~50 µs | ~50 ns |
|
||||
| After stdlib generator (~5K stdlib + 10K project) | ~75 µs | ~50 ns |
|
||||
|
||||
For a 1M-LOC project with ~5M method-call resolutions:
|
||||
- Linear scan: 5M × 75 µs = **375 sec** in lookups alone
|
||||
- Hash: 5M × 50 ns = **0.25 sec**
|
||||
|
||||
### Data structure
|
||||
|
||||
```c
|
||||
typedef struct CBMRegistryHashEntry {
|
||||
uint64_t hash; // FNV-1a or xxhash of (qualified_name)
|
||||
int func_index; // -1 if type, else index into reg->funcs[]
|
||||
int type_index; // -1 if func, else index into reg->types[]
|
||||
struct CBMRegistryHashEntry *next; // chain for receiver+name overload list
|
||||
} CBMRegistryHashEntry;
|
||||
|
||||
typedef struct CBMTypeRegistry {
|
||||
CBMRegisteredFunc* funcs;
|
||||
int func_count, func_cap;
|
||||
CBMRegisteredType* types;
|
||||
int type_count, type_cap;
|
||||
CBMArena* arena;
|
||||
|
||||
// NEW
|
||||
CBMRegistryHashEntry **func_buckets; // by qualified_name
|
||||
int func_bucket_count;
|
||||
CBMRegistryHashEntry **type_buckets; // by qualified_name
|
||||
int type_bucket_count;
|
||||
// Methods grouped by (receiver_type, short_name) for overload sets
|
||||
CBMRegistryHashEntry **method_buckets;
|
||||
int method_bucket_count;
|
||||
} CBMTypeRegistry;
|
||||
```
|
||||
|
||||
### Hash function
|
||||
|
||||
Use xxHash (already vendored at `internal/cbm/vendored/xxhash`). Single call
|
||||
per lookup: `XXH64(qualified_name, strlen, 0)`. Fast on modern CPUs (~10
|
||||
GB/s).
|
||||
|
||||
### Resize policy
|
||||
|
||||
- Initial bucket count: 1024 (handles small projects)
|
||||
- Grow at 75% load
|
||||
- Grow factor: 2× (rounded to next power of two)
|
||||
|
||||
### Linear-scan retained for
|
||||
|
||||
- Iteration code paths (e.g., "list all methods of type X")
|
||||
- Pattern-match queries (e.g., "find all funcs with name matching regex")
|
||||
|
||||
These are rare enough that O(n) is acceptable.
|
||||
|
||||
### API stays the same
|
||||
|
||||
`cbm_registry_lookup_method`, `cbm_registry_lookup_func`, etc. all keep their
|
||||
signatures. Internal implementation switches to hash-table probe.
|
||||
|
||||
---
|
||||
|
||||
## 5. Parallelism model
|
||||
|
||||
### Pass 1.5 (registry build)
|
||||
|
||||
Sequential — but tiny (~0.5 sec). Each file's defs are in its `CBMFileResult`
|
||||
already; just walk and insert.
|
||||
|
||||
If sequential is a bottleneck on >10M LOC projects, sharded build with
|
||||
per-thread sub-registries merged at the end is bounded extra work (~150 LOC).
|
||||
|
||||
### Pass 2.5 (resolution per file)
|
||||
|
||||
Parallel via worker pool. Each worker:
|
||||
- Holds a thread-local `TSLSPContext`
|
||||
- Reads from the shared project-wide registry (read-only after pass 1.5)
|
||||
- Writes to its own file's `CBMFileResult.resolved_calls` (no contention)
|
||||
|
||||
Existing precedent: `cbm_pipeline_pass_calls` already runs file-parallel via
|
||||
`cbm_worker_pool_dispatch`. Same pattern reused.
|
||||
|
||||
---
|
||||
|
||||
## 6. Memory budget
|
||||
|
||||
| Component | Single-file (today) | Cross-file (target) |
|
||||
|---|---|---|
|
||||
| Per-file arena | ~1 MB avg | ~1 MB avg (unchanged) |
|
||||
| Per-file registry | ~50 KB | gone (shared) |
|
||||
| Project-wide registry | n/a | ~2 MB stdlib + ~10 MB user defs (1M-LOC project) |
|
||||
| Hash buckets | n/a | ~200 KB total |
|
||||
|
||||
**Net memory delta: +12 MB on a 1M-LOC project.** Negligible relative to the
|
||||
existing ~500 MB peak the pipeline uses.
|
||||
|
||||
---
|
||||
|
||||
## 7. Latency budget
|
||||
|
||||
| Operation | Today | Naive cross-file | Target architecture |
|
||||
|---|---|---|---|
|
||||
| Parse + extract | 12.0 sec | 12.0 sec | 12.0 sec |
|
||||
| Per-file LSP (single-file) | 3.0 sec | n/a | n/a |
|
||||
| Pass 1.5 build registry | 0.0 sec | 0.5 sec | 0.5 sec |
|
||||
| Pass 2.5 LSP resolve | n/a | 18.0 sec | 2.0 sec |
|
||||
| **Total** | **15.0 sec** | **30.5 sec** | **14.5 sec** |
|
||||
| **Delta** | baseline | +103% | **−3% (parallelism wins back)** |
|
||||
|
||||
Negative delta is plausible because pass 2.5 is more parallelizable than
|
||||
single-file LSP (which runs interleaved with extraction today). Realistic
|
||||
target: **+0% to +10%**.
|
||||
|
||||
### Latency gates (rejection criteria)
|
||||
|
||||
- Total indexing time on TS bench corpus increases by >25%: revert.
|
||||
- Memory peak increases by >10%: revert.
|
||||
- Resolution coverage doesn't increase by ≥+5%: question whether the
|
||||
cross-file pass is doing useful work.
|
||||
|
||||
---
|
||||
|
||||
## 8. Backward compatibility
|
||||
|
||||
- `cbm_run_ts_lsp` keeps its existing signature (single-file mode).
|
||||
- `cbm_run_ts_lsp_cross` keeps its existing signature (manual-defs mode for
|
||||
tests).
|
||||
- New entry: `cbm_pipeline_pass_lsp_resolve(ctx, files, count, registry)` —
|
||||
internal pipeline-only.
|
||||
- Existing 268 tests must pass unchanged.
|
||||
- `CBM_LSP_DISABLED=1` env knob still respected (skip pass 2.5 entirely).
|
||||
|
||||
---
|
||||
|
||||
## 9. Failure modes & mitigations
|
||||
|
||||
| Failure | Probability | Mitigation |
|
||||
|---|---|---|
|
||||
| Hash collision storm (adversarial QN strings) | Very low | xxHash mixes well; no security risk |
|
||||
| Registry growth blows memory cap | Low | Estimate up-front from def_count × avg_size; pre-size buckets |
|
||||
| Cycle in re-export chain (after task #10) | Medium | Cycle guard at 16 hops, same as alias chain |
|
||||
| Stale cached_tree after edit (incremental indexing) | Medium | Existing `cbm_free_tree` invalidation chain handles this |
|
||||
| Worker pool starvation on small projects (<10 files) | Low | Fall back to sequential when file_count < worker_thread_count × 4 |
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation order (per plan task IDs)
|
||||
|
||||
1. **Task #1 (this document)** — design, no code.
|
||||
2. **Task #2** — hash-table registry. Standalone; doesn't change behavior,
|
||||
just data structure. Validates with existing 268 tests.
|
||||
3. **Task #6** — pass_lsp_resolve pipeline wiring. Uses #2's hash registry.
|
||||
First user-visible benefit.
|
||||
4. **Tasks #3, #4, #5, #7, #8, #9, #10, #11** — each independently builds on
|
||||
the foundation.
|
||||
5. **Task #12** — final benchmark.
|
||||
|
||||
---
|
||||
|
||||
## 11. Out of scope for this design
|
||||
|
||||
- **Incremental indexing.** Reusing the cross-file registry across watch-mode
|
||||
re-indexes is a follow-up — current scope is full-project indexing.
|
||||
- **Distributed/federated registries.** All-in-process for now.
|
||||
- **Cross-language LSP coordination.** Each language LSP gets its own
|
||||
registry; no shared state across Go/C/C++/TS LSPs.
|
||||
- **Memory-mapped registry persistence.** The registry is fully in-RAM and
|
||||
rebuilt every indexing run.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@
|
||||
#include "lsp/c_lsp.h"
|
||||
#include "lsp/php_lsp.h"
|
||||
#include "lsp/py_lsp.h"
|
||||
#include "lsp/ts_lsp.h"
|
||||
#include "preprocessor.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
|
||||
@@ -365,6 +366,23 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
|
||||
if (language == CBM_LANG_PYTHON) {
|
||||
cbm_run_py_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
if (language == CBM_LANG_JAVASCRIPT || language == CBM_LANG_TYPESCRIPT ||
|
||||
language == CBM_LANG_TSX) {
|
||||
bool js_mode = (language == CBM_LANG_JAVASCRIPT);
|
||||
// jsx_mode: TSX always; .jsx in the JS bucket also enables it.
|
||||
bool jsx_mode = (language == CBM_LANG_TSX);
|
||||
if (language == CBM_LANG_JAVASCRIPT && rel_path) {
|
||||
size_t rl = strlen(rel_path);
|
||||
if (rl >= 4 && strcmp(rel_path + rl - 4, ".jsx") == 0) jsx_mode = true;
|
||||
}
|
||||
// dts_mode: ".d.ts" suffix (TypeScript only).
|
||||
bool dts_mode = false;
|
||||
if (language == CBM_LANG_TYPESCRIPT && rel_path) {
|
||||
size_t rl = strlen(rel_path);
|
||||
if (rl >= 5 && strcmp(rel_path + rl - 5, ".d.ts") == 0) dts_mode = true;
|
||||
}
|
||||
cbm_run_ts_lsp(a, result, source, source_len, root, js_mode, jsx_mode, dts_mode);
|
||||
}
|
||||
atomic_fetch_add(&total_lsp_ns, now_ns() - lsp_start);
|
||||
|
||||
// Second pass: preprocess C/C++/CUDA and extract additional macro-hidden calls.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,136 @@
|
||||
#ifndef CBM_LSP_TS_LSP_H
|
||||
#define CBM_LSP_TS_LSP_H
|
||||
|
||||
#include "type_rep.h"
|
||||
#include "scope.h"
|
||||
#include "type_registry.h"
|
||||
#include "../cbm.h"
|
||||
#include "go_lsp.h" // for CBMLSPDef, CBMResolvedCallArray (shared cross-language)
|
||||
|
||||
// TSLSPContext holds state for TypeScript / JavaScript / JSX / TSX expression type
|
||||
// evaluation within a single file. One context per cbm_run_ts_lsp invocation.
|
||||
//
|
||||
// Mode flags choose dialect behaviour:
|
||||
// - js_mode: .js / .jsx — JSDoc inference is the primary type source; missing annotations
|
||||
// default to UNKNOWN rather than failing.
|
||||
// - jsx_mode: .jsx / .tsx — JSX expressions are recognised; intrinsic elements map to
|
||||
// builtin types; component calls register through the registry.
|
||||
// - dts_mode: .d.ts ambient declarations — no function bodies; populate registry only,
|
||||
// emit no resolved calls.
|
||||
//
|
||||
// Modes are independent: a `.tsx` file sets jsx_mode=true with js_mode=false; a `.jsx` file
|
||||
// sets both true.
|
||||
typedef struct {
|
||||
CBMArena* arena;
|
||||
const char* source;
|
||||
int source_len;
|
||||
const CBMTypeRegistry* registry;
|
||||
CBMScope* current_scope;
|
||||
|
||||
// Import map: local_name -> module QN (resolved or opaque).
|
||||
// Parallel arrays of length import_count.
|
||||
const char** import_local_names;
|
||||
const char** import_module_qns;
|
||||
int import_count;
|
||||
|
||||
// File / surrounding context.
|
||||
const char* module_qn; // QN of this file's module
|
||||
const char* enclosing_func_qn; // current function being walked, NULL at module scope
|
||||
const char* enclosing_class_qn; // current class for `this` resolution
|
||||
|
||||
// Output: resolved calls accumulate here.
|
||||
CBMResolvedCallArray* resolved_calls;
|
||||
|
||||
// Type-parameter scope (innermost generic function/class).
|
||||
// type_param_constraints may be NULL or shorter — entries default to "any".
|
||||
const char** type_param_names;
|
||||
const CBMType** type_param_constraints;
|
||||
int type_param_count;
|
||||
|
||||
// Mode flags — see comment above.
|
||||
bool js_mode;
|
||||
bool jsx_mode;
|
||||
bool dts_mode;
|
||||
bool strict; // tsconfig "strict": true → fewer implicit-any fallbacks
|
||||
bool debug; // CBM_LSP_DEBUG env
|
||||
|
||||
// Recursion guard for ts_eval_expr_type (mirrors c_lsp).
|
||||
int eval_depth;
|
||||
} TSLSPContext;
|
||||
|
||||
// --- Initialization ---
|
||||
|
||||
// Initialise a TSLSPContext for processing one file. Mode flags select dialect.
|
||||
void ts_lsp_init(TSLSPContext* ctx, CBMArena* arena, const char* source, int source_len,
|
||||
const CBMTypeRegistry* registry, const char* module_qn,
|
||||
bool js_mode, bool jsx_mode, bool dts_mode,
|
||||
CBMResolvedCallArray* out);
|
||||
|
||||
// Register an import: local binding name → module QN (or unresolved module specifier).
|
||||
void ts_lsp_add_import(TSLSPContext* ctx, const char* local_name, const char* module_qn);
|
||||
|
||||
// Walk the entire file: bind module-level declarations, then process every function /
|
||||
// method body. Safe on any tree-sitter root; emits zero calls in dts_mode.
|
||||
void ts_lsp_process_file(TSLSPContext* ctx, TSNode root);
|
||||
|
||||
// --- Internals exposed for tests and stdlib data ---
|
||||
|
||||
// Evaluate the type of a TS/JS expression node. Returns cbm_type_unknown() on miss.
|
||||
const CBMType* ts_eval_expr_type(TSLSPContext* ctx, TSNode node);
|
||||
|
||||
// Parse a TS/JS type-position AST node into a CBMType.
|
||||
const CBMType* ts_parse_type_node(TSLSPContext* ctx, TSNode node);
|
||||
|
||||
// Process a single statement node, binding any variables it declares into ctx->current_scope.
|
||||
void ts_process_statement(TSLSPContext* ctx, TSNode node);
|
||||
|
||||
// --- Entry points ---
|
||||
|
||||
// Single-file LSP. Builds a registry from result->defs + the TS stdlib subset, runs
|
||||
// resolution, and writes resolved calls into result->resolved_calls.
|
||||
void cbm_run_ts_lsp(CBMArena* arena, CBMFileResult* result,
|
||||
const char* source, int source_len, TSNode root,
|
||||
bool js_mode, bool jsx_mode, bool dts_mode);
|
||||
|
||||
// Cross-file LSP. Caller passes in cross-file definitions (CBMLSPDef list) and the
|
||||
// resolved import → module-QN map. Re-parses source if cached_tree is NULL.
|
||||
void cbm_run_ts_lsp_cross(CBMArena* arena,
|
||||
const char* source, int source_len,
|
||||
const char* module_qn,
|
||||
bool js_mode, bool jsx_mode, bool dts_mode,
|
||||
CBMLSPDef* defs, int def_count,
|
||||
const char** import_names, const char** import_qns, int import_count,
|
||||
TSTree* cached_tree,
|
||||
CBMResolvedCallArray* out);
|
||||
|
||||
// Register the TypeScript / JavaScript stdlib subset (Promise, Array<T>, Map<K,V>, Set<T>,
|
||||
// Object, Function, console, JSON) into a registry. v1 is hand-curated; a generator script
|
||||
// will replace this in v1.3.
|
||||
void cbm_ts_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena);
|
||||
|
||||
// --- Batch cross-file LSP ---
|
||||
|
||||
// Per-file input for batch TS LSP processing.
|
||||
typedef struct {
|
||||
const char* source;
|
||||
int source_len;
|
||||
const char* module_qn;
|
||||
bool js_mode;
|
||||
bool jsx_mode;
|
||||
bool dts_mode;
|
||||
TSTree* cached_tree; // from TSTree caching (NULL = parse internally)
|
||||
CBMLSPDef* defs; // combined file-local + cross-file defs
|
||||
int def_count;
|
||||
const char** import_names; // parallel arrays, import_count long
|
||||
const char** import_qns;
|
||||
int import_count;
|
||||
} CBMBatchTSLSPFile;
|
||||
|
||||
// Process multiple TS/JS files' cross-file LSP in one CGo call.
|
||||
// out must point to file_count pre-zeroed CBMResolvedCallArray structs.
|
||||
// Project-scope declaration merging happens here (per plan §17 finding #4).
|
||||
void cbm_batch_ts_lsp_cross(CBMArena* arena,
|
||||
CBMBatchTSLSPFile* files, int file_count,
|
||||
CBMResolvedCallArray* out);
|
||||
|
||||
#endif // CBM_LSP_TS_LSP_H
|
||||
@@ -2,6 +2,115 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// FNV-1a 64-bit hash. Public domain. Inline so no extra dep.
|
||||
static uint64_t fnv1a(const char* s) {
|
||||
uint64_t h = 0xcbf29ce484222325ULL;
|
||||
if (!s) return h;
|
||||
while (*s) {
|
||||
h ^= (unsigned char)*s++;
|
||||
h *= 0x100000001b3ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static uint64_t fnv1a_pair(const char* a, const char* b) {
|
||||
uint64_t h = 0xcbf29ce484222325ULL;
|
||||
if (a) {
|
||||
while (*a) {
|
||||
h ^= (unsigned char)*a++;
|
||||
h *= 0x100000001b3ULL;
|
||||
}
|
||||
}
|
||||
h ^= 0xff; // separator
|
||||
h *= 0x100000001b3ULL;
|
||||
if (b) {
|
||||
while (*b) {
|
||||
h ^= (unsigned char)*b++;
|
||||
h *= 0x100000001b3ULL;
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static int next_pow2(int n) {
|
||||
int p = 1;
|
||||
while (p < n) p <<= 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
static void build_qn_index(CBMTypeRegistry* reg, bool for_funcs) {
|
||||
int count = for_funcs ? reg->func_count : reg->type_count;
|
||||
if (count == 0) return;
|
||||
int bucket_count = next_pow2(count * 2);
|
||||
if (bucket_count < 16) bucket_count = 16;
|
||||
|
||||
int* buckets = (int*)cbm_arena_alloc(reg->arena, (size_t)bucket_count * sizeof(int));
|
||||
CBMRegistryHashEntry* entries = (CBMRegistryHashEntry*)cbm_arena_alloc(
|
||||
reg->arena, (size_t)count * sizeof(CBMRegistryHashEntry));
|
||||
if (!buckets || !entries) return;
|
||||
for (int i = 0; i < bucket_count; i++) buckets[i] = -1;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* qn = for_funcs ? reg->funcs[i].qualified_name
|
||||
: reg->types[i].qualified_name;
|
||||
if (!qn) continue;
|
||||
uint64_t h = fnv1a(qn);
|
||||
int slot = (int)(h & (uint64_t)(bucket_count - 1));
|
||||
entries[i].hash = h;
|
||||
entries[i].payload_index = i;
|
||||
entries[i].next_index = buckets[slot];
|
||||
entries[i].slot = slot;
|
||||
buckets[slot] = i;
|
||||
}
|
||||
if (for_funcs) {
|
||||
reg->func_qn_buckets = buckets;
|
||||
reg->func_qn_entries = entries;
|
||||
reg->func_qn_bucket_count = bucket_count;
|
||||
reg->func_qn_entry_count = count;
|
||||
} else {
|
||||
reg->type_qn_buckets = buckets;
|
||||
reg->type_qn_entries = entries;
|
||||
reg->type_qn_bucket_count = bucket_count;
|
||||
reg->type_qn_entry_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
static void build_method_index(CBMTypeRegistry* reg) {
|
||||
if (reg->func_count == 0) return;
|
||||
int bucket_count = next_pow2(reg->func_count * 2);
|
||||
if (bucket_count < 16) bucket_count = 16;
|
||||
int* buckets = (int*)cbm_arena_alloc(reg->arena, (size_t)bucket_count * sizeof(int));
|
||||
CBMRegistryHashEntry* entries = (CBMRegistryHashEntry*)cbm_arena_alloc(
|
||||
reg->arena, (size_t)reg->func_count * sizeof(CBMRegistryHashEntry));
|
||||
if (!buckets || !entries) return;
|
||||
for (int i = 0; i < bucket_count; i++) buckets[i] = -1;
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < reg->func_count; i++) {
|
||||
const CBMRegisteredFunc* f = ®->funcs[i];
|
||||
if (!f->receiver_type || !f->short_name) continue;
|
||||
uint64_t h = fnv1a_pair(f->receiver_type, f->short_name);
|
||||
int slot = (int)(h & (uint64_t)(bucket_count - 1));
|
||||
entries[idx].hash = h;
|
||||
entries[idx].payload_index = i;
|
||||
entries[idx].next_index = buckets[slot];
|
||||
entries[idx].slot = slot;
|
||||
buckets[slot] = idx;
|
||||
idx++;
|
||||
}
|
||||
reg->method_buckets = buckets;
|
||||
reg->method_entries = entries;
|
||||
reg->method_bucket_count = bucket_count;
|
||||
reg->method_entry_count = idx;
|
||||
}
|
||||
|
||||
void cbm_registry_finalize(CBMTypeRegistry* reg) {
|
||||
if (!reg) return;
|
||||
build_qn_index(reg, /*for_funcs=*/true);
|
||||
build_qn_index(reg, /*for_funcs=*/false);
|
||||
build_method_index(reg);
|
||||
}
|
||||
|
||||
void cbm_registry_init(CBMTypeRegistry* reg, CBMArena* arena) {
|
||||
memset(reg, 0, sizeof(CBMTypeRegistry));
|
||||
reg->arena = arena;
|
||||
@@ -41,6 +150,23 @@ const CBMRegisteredFunc* cbm_registry_lookup_method(const CBMTypeRegistry* reg,
|
||||
const char* receiver_qn, const char* method_name) {
|
||||
if (!reg || !receiver_qn || !method_name) return NULL;
|
||||
|
||||
// Hashed path when registry is finalized.
|
||||
if (reg->method_buckets && reg->method_bucket_count > 0) {
|
||||
uint64_t h = fnv1a_pair(receiver_qn, method_name);
|
||||
int slot = (int)(h & (uint64_t)(reg->method_bucket_count - 1));
|
||||
for (int idx = reg->method_buckets[slot]; idx >= 0;
|
||||
idx = reg->method_entries[idx].next_index) {
|
||||
if (reg->method_entries[idx].hash != h) continue;
|
||||
const CBMRegisteredFunc* f = ®->funcs[reg->method_entries[idx].payload_index];
|
||||
if (f->receiver_type && f->short_name &&
|
||||
strcmp(f->receiver_type, receiver_qn) == 0 &&
|
||||
strcmp(f->short_name, method_name) == 0) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < reg->func_count; i++) {
|
||||
const CBMRegisteredFunc* f = ®->funcs[i];
|
||||
if (f->receiver_type && f->short_name &&
|
||||
@@ -56,6 +182,21 @@ const CBMRegisteredType* cbm_registry_lookup_type(const CBMTypeRegistry* reg,
|
||||
const char* qualified_name) {
|
||||
if (!reg || !qualified_name) return NULL;
|
||||
|
||||
if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) {
|
||||
uint64_t h = fnv1a(qualified_name);
|
||||
int slot = (int)(h & (uint64_t)(reg->type_qn_bucket_count - 1));
|
||||
for (int idx = reg->type_qn_buckets[slot]; idx >= 0;
|
||||
idx = reg->type_qn_entries[idx].next_index) {
|
||||
if (reg->type_qn_entries[idx].hash != h) continue;
|
||||
int p = reg->type_qn_entries[idx].payload_index;
|
||||
if (reg->types[p].qualified_name &&
|
||||
strcmp(reg->types[p].qualified_name, qualified_name) == 0) {
|
||||
return ®->types[p];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < reg->type_count; i++) {
|
||||
if (strcmp(reg->types[i].qualified_name, qualified_name) == 0) {
|
||||
return ®->types[i];
|
||||
@@ -68,6 +209,21 @@ const CBMRegisteredFunc* cbm_registry_lookup_func(const CBMTypeRegistry* reg,
|
||||
const char* qualified_name) {
|
||||
if (!reg || !qualified_name) return NULL;
|
||||
|
||||
if (reg->func_qn_buckets && reg->func_qn_bucket_count > 0) {
|
||||
uint64_t h = fnv1a(qualified_name);
|
||||
int slot = (int)(h & (uint64_t)(reg->func_qn_bucket_count - 1));
|
||||
for (int idx = reg->func_qn_buckets[slot]; idx >= 0;
|
||||
idx = reg->func_qn_entries[idx].next_index) {
|
||||
if (reg->func_qn_entries[idx].hash != h) continue;
|
||||
int p = reg->func_qn_entries[idx].payload_index;
|
||||
if (reg->funcs[p].qualified_name &&
|
||||
strcmp(reg->funcs[p].qualified_name, qualified_name) == 0) {
|
||||
return ®->funcs[p];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < reg->func_count; i++) {
|
||||
if (strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
|
||||
return ®->funcs[i];
|
||||
@@ -313,3 +469,54 @@ const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_args(const CBMTypeRegistr
|
||||
}
|
||||
return range_match ? range_match : first_match;
|
||||
}
|
||||
|
||||
// --- TS-specific helpers ---
|
||||
|
||||
const CBMRegisteredFunc* cbm_registry_lookup_callable(const CBMTypeRegistry* reg,
|
||||
CBMArena* arena, const char* type_qn) {
|
||||
if (!reg || !type_qn || !arena) return NULL;
|
||||
|
||||
const CBMRegisteredType* rt = cbm_registry_lookup_type(reg, type_qn);
|
||||
if (!rt || !rt->call_signature) return NULL;
|
||||
|
||||
// Synthesise a CBMRegisteredFunc whose QN is "<type_qn>.__call".
|
||||
// Allocated in the caller's arena so it lives at least as long as the registry context.
|
||||
size_t qn_len = strlen(type_qn);
|
||||
const char suffix[] = ".__call";
|
||||
size_t total = qn_len + sizeof(suffix); // sizeof includes the NUL
|
||||
char* qn = (char*)cbm_arena_alloc(arena, total);
|
||||
if (!qn) return NULL;
|
||||
memcpy(qn, type_qn, qn_len);
|
||||
memcpy(qn + qn_len, suffix, sizeof(suffix));
|
||||
|
||||
CBMRegisteredFunc* f = (CBMRegisteredFunc*)cbm_arena_alloc(arena, sizeof(CBMRegisteredFunc));
|
||||
if (!f) return NULL;
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->qualified_name = qn;
|
||||
f->short_name = "__call";
|
||||
f->receiver_type = rt->qualified_name;
|
||||
f->signature = rt->call_signature;
|
||||
f->min_params = -1;
|
||||
return f;
|
||||
}
|
||||
|
||||
const CBMType* cbm_registry_lookup_index_signature(const CBMTypeRegistry* reg,
|
||||
const char* type_qn, const CBMType* key_type) {
|
||||
if (!reg || !type_qn) return NULL;
|
||||
|
||||
const CBMRegisteredType* rt = cbm_registry_lookup_type(reg, type_qn);
|
||||
if (!rt || !rt->index_value_type) return NULL;
|
||||
|
||||
// If no expected key type is recorded, accept any index. Otherwise require a match
|
||||
// on BUILTIN tag — TS allows string and number index signatures and one of each.
|
||||
if (!rt->index_key_type || !key_type) return rt->index_value_type;
|
||||
|
||||
if (rt->index_key_type->kind == CBM_TYPE_BUILTIN && key_type->kind == CBM_TYPE_BUILTIN) {
|
||||
const char* a = rt->index_key_type->data.builtin.name;
|
||||
const char* b = key_type->data.builtin.name;
|
||||
if (a && b && strcmp(a, b) == 0) return rt->index_value_type;
|
||||
// String index also accepts numeric keys (number → toString).
|
||||
if (a && strcmp(a, "string") == 0) return rt->index_value_type;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -43,8 +43,25 @@ typedef struct {
|
||||
const char* alias_of; // QN of aliased type (type Foo = Bar), NULL if not alias
|
||||
const char** type_param_names; // NULL-terminated, e.g., ["T", "K", NULL] for template classes
|
||||
bool is_interface;
|
||||
|
||||
// --- TS-specific fields (NULL/empty for non-TS types — backward compatible) ---
|
||||
// TS interfaces / object types may be callable: `interface F { (x:number): string }`.
|
||||
const CBMType* call_signature; // FUNC type or NULL
|
||||
// TS objects can have an index signature: `{ [key:string]: V }` or `{ [i:number]: V }`.
|
||||
const CBMType* index_key_type; // BUILTIN("string"|"number") or NULL
|
||||
const CBMType* index_value_type; // V or NULL
|
||||
// Generic constraints, parallel to type_param_names. NULL or shorter array means "any".
|
||||
const CBMType** type_param_constraints; // NULL-terminated, parallel to type_param_names
|
||||
} CBMRegisteredType;
|
||||
|
||||
// Hash-table bucket entry. Chains collisions via next-index list for overload sets.
|
||||
typedef struct {
|
||||
uint64_t hash; // FNV-1a of key
|
||||
int payload_index; // index into reg->funcs[] or reg->types[]
|
||||
int next_index; // -1 = end of chain; else index of next bucket entry in same chain
|
||||
int slot; // bucket slot this entry sits in (for resize)
|
||||
} CBMRegistryHashEntry;
|
||||
|
||||
// Cross-file type/function registry.
|
||||
typedef struct {
|
||||
CBMRegisteredFunc* funcs;
|
||||
@@ -56,11 +73,36 @@ typedef struct {
|
||||
int type_cap;
|
||||
|
||||
CBMArena* arena; // owns all string data
|
||||
|
||||
// Hash indexes (built lazily by cbm_registry_finalize, NULL until then).
|
||||
// Lookups fall back to linear scan when these are NULL.
|
||||
int* func_qn_buckets; // bucket → first entry index in func_qn_entries; -1 = empty
|
||||
CBMRegistryHashEntry* func_qn_entries; // entries indexed by linear order
|
||||
int func_qn_bucket_count;
|
||||
int func_qn_entry_count;
|
||||
|
||||
int* type_qn_buckets;
|
||||
CBMRegistryHashEntry* type_qn_entries;
|
||||
int type_qn_bucket_count;
|
||||
int type_qn_entry_count;
|
||||
|
||||
// Methods indexed by (receiver_type, short_name) — chain holds overloads.
|
||||
int* method_buckets;
|
||||
CBMRegistryHashEntry* method_entries;
|
||||
int method_bucket_count;
|
||||
int method_entry_count;
|
||||
} CBMTypeRegistry;
|
||||
|
||||
// Initialize a registry.
|
||||
void cbm_registry_init(CBMTypeRegistry* reg, CBMArena* arena);
|
||||
|
||||
// Build the hash indexes after all funcs/types have been added. Subsequent lookups
|
||||
// use O(1) hashed dispatch instead of linear scans. Calling this is OPTIONAL — the
|
||||
// linear-scan path remains correct. Single-file resolvers (small registries) skip
|
||||
// finalize and stay linear; project-wide registries (many thousands of entries) call
|
||||
// it once after pass-1.5 def-collection.
|
||||
void cbm_registry_finalize(CBMTypeRegistry* reg);
|
||||
|
||||
// Register a function/method.
|
||||
void cbm_registry_add_func(CBMTypeRegistry* reg, CBMRegisteredFunc func);
|
||||
|
||||
@@ -111,4 +153,18 @@ const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_types(const CBMTypeRegist
|
||||
const char* package_qn, const char* name,
|
||||
const CBMType** arg_types, int arg_count);
|
||||
|
||||
// --- TS-specific helpers (return NULL for types without these signatures) ---
|
||||
|
||||
// If the type has a call signature (e.g., `interface F { (x:number): string }`), return
|
||||
// a synthesised CBMRegisteredFunc whose qualified_name is "<type_qn>.__call" and
|
||||
// short_name is "__call". Returns NULL if no call signature is present, the type is
|
||||
// missing, or the receiver type was not registered. Caller must NOT free.
|
||||
const CBMRegisteredFunc* cbm_registry_lookup_callable(const CBMTypeRegistry* reg,
|
||||
CBMArena* arena, const char* type_qn);
|
||||
|
||||
// If the type has an index signature, return the value type produced by indexing with
|
||||
// the given key type (string vs number). Returns NULL if no matching index signature.
|
||||
const CBMType* cbm_registry_lookup_index_signature(const CBMTypeRegistry* reg,
|
||||
const char* type_qn, const CBMType* key_type);
|
||||
|
||||
#endif // CBM_LSP_TYPE_REGISTRY_H
|
||||
+227
-1
@@ -197,6 +197,8 @@ static int union_member_dedupe(const CBMType** scratch, int count) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Shared by Python (cbm_type_union) and TS (`A | B`). Flattens nested UNIONs and
|
||||
// dedupes members.
|
||||
const CBMType* cbm_type_union(CBMArena* a, const CBMType** members, int count) {
|
||||
if (!members || count <= 0) return &unknown_singleton;
|
||||
|
||||
@@ -309,6 +311,128 @@ const CBMType* cbm_type_callable(CBMArena* a, const CBMType** param_types, int p
|
||||
return t;
|
||||
}
|
||||
|
||||
// --- TS-specific constructors -----------------------------------------------
|
||||
|
||||
const CBMType* cbm_type_intersection(CBMArena* a, const CBMType** members, int count) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_INTERSECTION;
|
||||
if (members && count > 0) {
|
||||
const CBMType** arr = (const CBMType**)cbm_arena_alloc(a, (count + 1) * sizeof(const CBMType*));
|
||||
if (arr) {
|
||||
for (int i = 0; i < count; i++) arr[i] = members[i];
|
||||
arr[count] = NULL;
|
||||
t->data.union_type.members = arr;
|
||||
}
|
||||
}
|
||||
t->data.union_type.count = count;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_ts_literal(CBMArena* a, const char* tag, const char* value) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_TS_LITERAL;
|
||||
t->data.literal_ts.tag = tag ? cbm_arena_strdup(a, tag) : NULL;
|
||||
t->data.literal_ts.value = value ? cbm_arena_strdup(a, value) : NULL;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_indexed(CBMArena* a, const CBMType* object, const CBMType* index) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_INDEXED;
|
||||
t->data.indexed.object = object;
|
||||
t->data.indexed.index = index;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_keyof(CBMArena* a, const CBMType* operand) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_KEYOF;
|
||||
t->data.keyof.operand = operand;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_typeof_query(CBMArena* a, const char* expr) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_TYPEOF_QUERY;
|
||||
t->data.typeof_query.expr = expr ? cbm_arena_strdup(a, expr) : NULL;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_conditional(CBMArena* a,
|
||||
const CBMType* check, const CBMType* extends,
|
||||
const CBMType* true_branch, const CBMType* false_branch) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_CONDITIONAL;
|
||||
t->data.conditional.check = check;
|
||||
t->data.conditional.extends = extends;
|
||||
t->data.conditional.true_branch = true_branch;
|
||||
t->data.conditional.false_branch = false_branch;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_object_lit(CBMArena* a,
|
||||
const char** prop_names, const CBMType** prop_types,
|
||||
const CBMType* call_signature, const CBMType* index_value) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_OBJECT_LIT;
|
||||
if (prop_names && prop_types) {
|
||||
int count = 0;
|
||||
while (prop_names[count] && prop_types[count]) count++;
|
||||
if (count > 0) {
|
||||
const char** name_arr = (const char**)cbm_arena_alloc(a, (count + 1) * sizeof(const char*));
|
||||
const CBMType** type_arr = (const CBMType**)cbm_arena_alloc(a, (count + 1) * sizeof(const CBMType*));
|
||||
if (name_arr && type_arr) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
name_arr[i] = prop_names[i];
|
||||
type_arr[i] = prop_types[i];
|
||||
}
|
||||
name_arr[count] = NULL;
|
||||
type_arr[count] = NULL;
|
||||
t->data.object_lit.prop_names = name_arr;
|
||||
t->data.object_lit.prop_types = type_arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
t->data.object_lit.call_signature = call_signature;
|
||||
t->data.object_lit.index_value = index_value;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_infer(CBMArena* a, const char* name) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_INFER;
|
||||
t->data.infer.name = name ? cbm_arena_strdup(a, name) : NULL;
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType* cbm_type_mapped(CBMArena* a,
|
||||
const char* key_name, const CBMType* key_constraint, const CBMType* value) {
|
||||
CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t) return &unknown_singleton;
|
||||
memset(t, 0, sizeof(CBMType));
|
||||
t->kind = CBM_TYPE_MAPPED;
|
||||
t->data.mapped.key_name = key_name ? cbm_arena_strdup(a, key_name) : NULL;
|
||||
t->data.mapped.key_constraint = key_constraint;
|
||||
t->data.mapped.value = value;
|
||||
return t;
|
||||
}
|
||||
|
||||
// Operations
|
||||
|
||||
const CBMType* cbm_type_deref(const CBMType* t) {
|
||||
@@ -451,6 +575,41 @@ bool cbm_type_equal(const CBMType* a, const CBMType* b) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CBM_TYPE_INTERSECTION: {
|
||||
// Same shape as UNION; compare order-independently.
|
||||
if (a->data.union_type.count != b->data.union_type.count) return false;
|
||||
for (int i = 0; i < a->data.union_type.count; i++) {
|
||||
bool found = false;
|
||||
for (int j = 0; j < b->data.union_type.count; j++) {
|
||||
if (cbm_type_equal(a->data.union_type.members[i],
|
||||
b->data.union_type.members[j])) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CBM_TYPE_TS_LITERAL:
|
||||
return str_eq_or_both_null(a->data.literal_ts.tag, b->data.literal_ts.tag)
|
||||
&& str_eq_or_both_null(a->data.literal_ts.value, b->data.literal_ts.value);
|
||||
case CBM_TYPE_INDEXED:
|
||||
return cbm_type_equal(a->data.indexed.object, b->data.indexed.object)
|
||||
&& cbm_type_equal(a->data.indexed.index, b->data.indexed.index);
|
||||
case CBM_TYPE_KEYOF:
|
||||
return cbm_type_equal(a->data.keyof.operand, b->data.keyof.operand);
|
||||
case CBM_TYPE_TYPEOF_QUERY:
|
||||
return str_eq_or_both_null(a->data.typeof_query.expr, b->data.typeof_query.expr);
|
||||
case CBM_TYPE_CONDITIONAL:
|
||||
return cbm_type_equal(a->data.conditional.check, b->data.conditional.check)
|
||||
&& cbm_type_equal(a->data.conditional.extends, b->data.conditional.extends)
|
||||
&& cbm_type_equal(a->data.conditional.true_branch, b->data.conditional.true_branch)
|
||||
&& cbm_type_equal(a->data.conditional.false_branch, b->data.conditional.false_branch);
|
||||
case CBM_TYPE_INFER:
|
||||
return str_eq_or_both_null(a->data.infer.name, b->data.infer.name);
|
||||
case CBM_TYPE_OBJECT_LIT:
|
||||
case CBM_TYPE_MAPPED:
|
||||
case CBM_TYPE_FUNC:
|
||||
case CBM_TYPE_INTERFACE:
|
||||
case CBM_TYPE_STRUCT:
|
||||
@@ -553,7 +712,74 @@ const CBMType* cbm_type_substitute(CBMArena* a, const CBMType* t,
|
||||
elems[count] = NULL;
|
||||
return cbm_type_tuple(a, elems, count);
|
||||
}
|
||||
case CBM_TYPE_UNION:
|
||||
case CBM_TYPE_INTERSECTION: {
|
||||
int count = t->data.union_type.count;
|
||||
if (count <= 0 || !t->data.union_type.members) return t;
|
||||
const CBMType** elems = (const CBMType**)cbm_arena_alloc(a, (count + 1) * sizeof(const CBMType*));
|
||||
if (!elems) return t;
|
||||
for (int i = 0; i < count; i++) {
|
||||
elems[i] = cbm_type_substitute(a, t->data.union_type.members[i], type_params, type_args);
|
||||
}
|
||||
elems[count] = NULL;
|
||||
return t->kind == CBM_TYPE_UNION
|
||||
? cbm_type_union(a, elems, count)
|
||||
: cbm_type_intersection(a, elems, count);
|
||||
}
|
||||
case CBM_TYPE_INDEXED:
|
||||
return cbm_type_indexed(a,
|
||||
cbm_type_substitute(a, t->data.indexed.object, type_params, type_args),
|
||||
cbm_type_substitute(a, t->data.indexed.index, type_params, type_args));
|
||||
case CBM_TYPE_KEYOF:
|
||||
return cbm_type_keyof(a, cbm_type_substitute(a, t->data.keyof.operand, type_params, type_args));
|
||||
case CBM_TYPE_CONDITIONAL:
|
||||
return cbm_type_conditional(a,
|
||||
cbm_type_substitute(a, t->data.conditional.check, type_params, type_args),
|
||||
cbm_type_substitute(a, t->data.conditional.extends, type_params, type_args),
|
||||
cbm_type_substitute(a, t->data.conditional.true_branch, type_params, type_args),
|
||||
cbm_type_substitute(a, t->data.conditional.false_branch, type_params, type_args));
|
||||
case CBM_TYPE_FUNC: {
|
||||
// Recurse into param_types and return_types. Param/return arrays may be NULL.
|
||||
const CBMType** new_params = NULL;
|
||||
const CBMType** new_returns = NULL;
|
||||
if (t->data.func.param_types) {
|
||||
int pc = 0;
|
||||
while (t->data.func.param_types[pc]) pc++;
|
||||
new_params = (const CBMType**)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(const CBMType*));
|
||||
if (!new_params) return t;
|
||||
for (int i = 0; i < pc; i++) {
|
||||
new_params[i] = cbm_type_substitute(a, t->data.func.param_types[i], type_params, type_args);
|
||||
}
|
||||
new_params[pc] = NULL;
|
||||
}
|
||||
if (t->data.func.return_types) {
|
||||
int rc = 0;
|
||||
while (t->data.func.return_types[rc]) rc++;
|
||||
new_returns = (const CBMType**)cbm_arena_alloc(a, (size_t)(rc + 1) * sizeof(const CBMType*));
|
||||
if (!new_returns) return t;
|
||||
for (int i = 0; i < rc; i++) {
|
||||
new_returns[i] = cbm_type_substitute(a, t->data.func.return_types[i], type_params, type_args);
|
||||
}
|
||||
new_returns[rc] = NULL;
|
||||
}
|
||||
return cbm_type_func(a, t->data.func.param_names, new_params, new_returns);
|
||||
}
|
||||
case CBM_TYPE_TEMPLATE: {
|
||||
if (!t->data.template_type.template_args || t->data.template_type.arg_count == 0) return t;
|
||||
int ac = t->data.template_type.arg_count;
|
||||
const CBMType** new_args = (const CBMType**)cbm_arena_alloc(a,
|
||||
(size_t)(ac + 1) * sizeof(const CBMType*));
|
||||
if (!new_args) return t;
|
||||
for (int i = 0; i < ac; i++) {
|
||||
new_args[i] = cbm_type_substitute(a, t->data.template_type.template_args[i],
|
||||
type_params, type_args);
|
||||
}
|
||||
new_args[ac] = NULL;
|
||||
return cbm_type_template(a, t->data.template_type.template_name, new_args, ac);
|
||||
}
|
||||
default:
|
||||
return t; // NAMED, BUILTIN, FUNC, etc. — no type params to substitute
|
||||
// BUILTIN, INTERFACE, STRUCT, LITERAL, TYPEOF_QUERY, OBJECT_LIT, INFER, MAPPED,
|
||||
// ALIAS — no in-place substitution needed at v1 (or stub-only).
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,29 @@ typedef enum {
|
||||
CBM_TYPE_INTERFACE, // interface{...}
|
||||
CBM_TYPE_STRUCT, // struct{...}
|
||||
CBM_TYPE_BUILTIN, // int, string, bool, error, etc.
|
||||
CBM_TYPE_TUPLE, // multi-return (T1, T2)
|
||||
CBM_TYPE_TUPLE, // multi-return (T1, T2) / TS tuple [T,U]
|
||||
CBM_TYPE_TYPE_PARAM, // generic type parameter: T, K, V
|
||||
CBM_TYPE_REFERENCE, // T& (C++ lvalue reference)
|
||||
CBM_TYPE_RVALUE_REF, // T&& (C++ rvalue reference)
|
||||
CBM_TYPE_TEMPLATE, // Parameterized type: vector<T> — stores template name + args
|
||||
CBM_TYPE_ALIAS, // Type alias: using/typedef — stores alias name + underlying type
|
||||
CBM_TYPE_UNION, // Python: A | B, Optional[T], Union[A, B] — sorted-canonical list
|
||||
CBM_TYPE_UNION, // Python: A | B; TS: A | B | C — sorted-canonical list (shared)
|
||||
CBM_TYPE_LITERAL, // Python: Literal["foo", 3] — wraps a base type + literal value text
|
||||
CBM_TYPE_PROTOCOL, // Python: typing.Protocol — like INTERFACE but matched structurally
|
||||
CBM_TYPE_MODULE, // Python: import os; os is a module-typed binding
|
||||
CBM_TYPE_CALLABLE, // Python: Callable[[A, B], R] — untyped-named callable variant of FUNC
|
||||
|
||||
// --- TS-specific kinds (added in TS LSP integration) ---
|
||||
CBM_TYPE_INTERSECTION, // TS: A & B — intersection type
|
||||
CBM_TYPE_TS_LITERAL, // TS: "foo" / 42 / true literal types (tag+value layout, distinct
|
||||
// from Python's CBM_TYPE_LITERAL which uses base+literal_text)
|
||||
CBM_TYPE_INDEXED, // TS: T[K] — indexed access type
|
||||
CBM_TYPE_KEYOF, // TS: keyof T
|
||||
CBM_TYPE_TYPEOF_QUERY, // TS: typeof x in type position
|
||||
CBM_TYPE_CONDITIONAL, // TS: T extends U ? X : Y
|
||||
CBM_TYPE_OBJECT_LIT, // TS: { a: T1; b: T2 } anonymous object type
|
||||
CBM_TYPE_INFER, // TS: `infer X` placeholder inside conditional
|
||||
CBM_TYPE_MAPPED, // TS: {[K in keyof T]: ...} — v1 stub, members may be NULL
|
||||
} CBMTypeKind;
|
||||
|
||||
// Forward declaration
|
||||
@@ -80,11 +92,11 @@ struct CBMType {
|
||||
struct {
|
||||
const CBMType** members; // NULL-terminated, deduplicated, sorted by kind/qn
|
||||
int count;
|
||||
} union_type; // UNION
|
||||
} union_type; // UNION / INTERSECTION (shared)
|
||||
struct {
|
||||
const CBMType* base; // base type (e.g. BUILTIN("int"), BUILTIN("str"))
|
||||
const char* literal_text; // canonical text: "3", "\"foo\"", "True"
|
||||
} literal; // LITERAL
|
||||
} literal; // LITERAL (Python)
|
||||
struct {
|
||||
const char* qualified_name; // e.g. "typing.Iterable"
|
||||
const char** method_names; // NULL-terminated method names — structural matching
|
||||
@@ -98,6 +110,38 @@ struct CBMType {
|
||||
const CBMType* return_type; // single return; for tuples wrap in CBM_TYPE_TUPLE
|
||||
int param_count; // -1 = elliptic / Callable[..., R]
|
||||
} callable; // CALLABLE
|
||||
|
||||
// --- TS-specific data ---
|
||||
struct {
|
||||
// Tag distinguishes string / number / boolean / bigint / null / undefined literals.
|
||||
// For boolean literals, value points to "true" or "false".
|
||||
const char* tag; // "string" | "number" | "boolean" | "bigint" | "null" | "undefined"
|
||||
const char* value; // textual representation; arena-owned
|
||||
} literal_ts; // TS_LITERAL
|
||||
struct {
|
||||
const CBMType* object; // T in T[K]
|
||||
const CBMType* index; // K in T[K]
|
||||
} indexed; // INDEXED
|
||||
struct { const CBMType* operand; } keyof; // KEYOF
|
||||
struct { const char* expr; } typeof_query; // TYPEOF_QUERY (referenced expression text)
|
||||
struct {
|
||||
const CBMType* check; // T
|
||||
const CBMType* extends; // U
|
||||
const CBMType* true_branch; // X
|
||||
const CBMType* false_branch; // Y
|
||||
} conditional; // CONDITIONAL
|
||||
struct {
|
||||
const char** prop_names; // NULL-terminated
|
||||
const CBMType** prop_types; // NULL-terminated, parallel to prop_names
|
||||
const CBMType* call_signature; // FUNC type or NULL
|
||||
const CBMType* index_value; // type produced by string/number index, or NULL
|
||||
} object_lit; // OBJECT_LIT
|
||||
struct { const char* name; } infer; // INFER (e.g., `infer R`)
|
||||
struct {
|
||||
const char* key_name; // "K" in {[K in keyof T]: V}
|
||||
const CBMType* key_constraint; // `keyof T`
|
||||
const CBMType* value; // V (may reference key_name as TYPE_PARAM)
|
||||
} mapped; // MAPPED (v1 stub-friendly)
|
||||
} data;
|
||||
};
|
||||
|
||||
@@ -120,6 +164,7 @@ const CBMType* cbm_type_alias(CBMArena* a, const char* alias_qn, const CBMType*
|
||||
// Python-flavored constructors. UNION normalizes input: nested unions are
|
||||
// flattened, duplicates removed, single-member unions collapse to that
|
||||
// member, and the empty union is UNKNOWN. Members must be arena-allocated.
|
||||
// Shared with TS LSP — both call this same constructor for `A | B`.
|
||||
const CBMType* cbm_type_union(CBMArena* a, const CBMType** members, int count);
|
||||
const CBMType* cbm_type_optional(CBMArena* a, const CBMType* t); // Optional[T] == Union[T, None]
|
||||
const CBMType* cbm_type_literal(CBMArena* a, const CBMType* base, const char* literal_text);
|
||||
@@ -129,6 +174,25 @@ const CBMType* cbm_type_module(CBMArena* a, const char* module_qn);
|
||||
const CBMType* cbm_type_callable(CBMArena* a, const CBMType** param_types, int param_count,
|
||||
const CBMType* return_type);
|
||||
|
||||
// --- TS-specific constructors ---
|
||||
const CBMType* cbm_type_intersection(CBMArena* a, const CBMType** members, int count);
|
||||
// tag is one of "string"|"number"|"boolean"|"bigint"|"null"|"undefined".
|
||||
// Distinct from cbm_type_literal (Python) which uses base+literal_text.
|
||||
const CBMType* cbm_type_ts_literal(CBMArena* a, const char* tag, const char* value);
|
||||
const CBMType* cbm_type_indexed(CBMArena* a, const CBMType* object, const CBMType* index);
|
||||
const CBMType* cbm_type_keyof(CBMArena* a, const CBMType* operand);
|
||||
const CBMType* cbm_type_typeof_query(CBMArena* a, const char* expr);
|
||||
const CBMType* cbm_type_conditional(CBMArena* a,
|
||||
const CBMType* check, const CBMType* extends,
|
||||
const CBMType* true_branch, const CBMType* false_branch);
|
||||
// prop_names and prop_types are NULL-terminated parallel arrays; either may be NULL for empty.
|
||||
const CBMType* cbm_type_object_lit(CBMArena* a,
|
||||
const char** prop_names, const CBMType** prop_types,
|
||||
const CBMType* call_signature, const CBMType* index_value);
|
||||
const CBMType* cbm_type_infer(CBMArena* a, const char* name);
|
||||
const CBMType* cbm_type_mapped(CBMArena* a,
|
||||
const char* key_name, const CBMType* key_constraint, const CBMType* value);
|
||||
|
||||
// Operations
|
||||
const CBMType* cbm_type_deref(const CBMType* t); // remove one pointer level
|
||||
const CBMType* cbm_type_elem(const CBMType* t); // get element type (slice/chan/pointer)
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
#include "lsp/generated/php_stdlib_data.c"
|
||||
#include "lsp/generated/python_stdlib_data.c"
|
||||
#include "lsp/py_lsp.c"
|
||||
#include "lsp/ts_lsp.c"
|
||||
@@ -49,6 +49,7 @@ extern void suite_py_lsp(void);
|
||||
extern void suite_py_lsp_bench(void);
|
||||
extern void suite_py_lsp_stress(void);
|
||||
extern void suite_py_lsp_scale(void);
|
||||
extern void suite_ts_lsp(void);
|
||||
extern void suite_store_arch(void);
|
||||
extern void suite_store_bulk(void);
|
||||
extern void suite_traces(void);
|
||||
@@ -131,6 +132,7 @@ int main(void) {
|
||||
RUN_SUITE(py_lsp_bench);
|
||||
RUN_SUITE(py_lsp_stress);
|
||||
RUN_SUITE(py_lsp_scale);
|
||||
RUN_SUITE(ts_lsp);
|
||||
|
||||
/* Architecture + ADR + Louvain */
|
||||
RUN_SUITE(store_arch);
|
||||
|
||||
+4360
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user