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:
Martin Vogel committed 2026-05-10 14:44:50 +02:00
1 parent 673ac4ef8a
commit d1143fbdce
14 files changed
+10903 -8

No files matched your search

+3 -1
View File
@@ -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 ────────────────────────────────────────────
+2 -2
View File
@@ -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
+302
View File
@@ -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
+18
View File
@@ -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
+136
View File
@@ -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
+207
View File
@@ -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 = &reg->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 = &reg->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 = &reg->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 &reg->types[p];
}
}
return NULL;
}
for (int i = 0; i < reg->type_count; i++) {
if (strcmp(reg->types[i].qualified_name, qualified_name) == 0) {
return &reg->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 &reg->funcs[p];
}
}
return NULL;
}
for (int i = 0; i < reg->func_count; i++) {
if (strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
return &reg->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;
}
+56
View File
@@ -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
View File
@@ -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;
}
}
+68 -4
View File
@@ -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)
+1
View File
@@ -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"
+2
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff