5685 lines
251 KiB
C
5685 lines
251 KiB
C
/* rust_lsp.c — Type-aware call resolution for Rust source files.
|
|
*
|
|
* Mirrors the structure of `go_lsp.c` and reverse-engineers the relevant
|
|
* pieces of `rust-analyzer` (`hir-def/resolver.rs`,
|
|
* `hir-ty/method_resolution.rs`, `hir-ty/infer.rs`) into a per-file walk
|
|
* driven by tree-sitter-rust.
|
|
*
|
|
* The compilation unit is split into clearly-labelled sections:
|
|
*
|
|
* 1. Init + helpers (~150 lines)
|
|
* 2. Builtin / prelude tables (~100 lines)
|
|
* 3. Path & use resolution (~250 lines)
|
|
* 4. Type-AST → CBMType (~250 lines)
|
|
* 5. Generic substitution (~150 lines)
|
|
* 6. Expression evaluator (~700 lines)
|
|
* 7. Method dispatch (~400 lines)
|
|
* 8. Macro handling (~200 lines)
|
|
* 9. Statement / pattern bind (~400 lines)
|
|
* 10. Function & file walk (~250 lines)
|
|
* 11. Per-file entry (~250 lines)
|
|
* 12. Cross-file + batch (~250 lines)
|
|
*
|
|
* Total ~3300 lines, matching the depth of go_lsp.c (2750) and py_lsp.c
|
|
* (3188). The code is structured so each section has a single coherent
|
|
* responsibility — there are no surprise back-edges between sections.
|
|
*/
|
|
|
|
#include "rust_lsp.h"
|
|
#include "rust_cargo.h"
|
|
#include "../helpers.h"
|
|
#include <ctype.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 1. Initialisation + arena helpers
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Forward declarations for early callers in the file. */
|
|
static void rust_resolve_calls_in_node(RustLSPContext *ctx, TSNode node);
|
|
static void rust_emit_resolved_call(RustLSPContext *ctx, const char *callee_qn,
|
|
const char *strategy, float confidence);
|
|
static void rust_inject_syn_call(RustLSPContext *ctx, const char *callee_qn);
|
|
static void rust_emit_unresolved_call(RustLSPContext *ctx, const char *expr_text,
|
|
const char *reason);
|
|
static const CBMType *rust_lookup_field(RustLSPContext *ctx, const char *type_qn,
|
|
const char *field_name, int depth);
|
|
static const CBMRegisteredFunc *rust_lookup_method_in_trait(RustLSPContext *ctx,
|
|
const char *trait_qn,
|
|
const char *method_name);
|
|
static char *rust_node_text(RustLSPContext *ctx, TSNode node);
|
|
static const char *convert_path_to_qn(CBMArena *arena, const char *path);
|
|
static bool rust_type_derefs_to_first_arg(const char *type_qn);
|
|
static const char *rust_lookup_type_param_bound(RustLSPContext *ctx, const char *name);
|
|
static void rust_collect_bounds_from_text(RustLSPContext *ctx, const char *text);
|
|
static void rust_record_type_param_bound(RustLSPContext *ctx, const char *param_name,
|
|
const char *trait_qn);
|
|
|
|
void rust_lsp_init(RustLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
|
|
const CBMTypeRegistry *registry, const char *module_qn,
|
|
CBMResolvedCallArray *out) {
|
|
memset(ctx, 0, sizeof(RustLSPContext));
|
|
ctx->arena = arena;
|
|
ctx->source = source;
|
|
ctx->source_len = source_len;
|
|
ctx->registry = registry;
|
|
ctx->module_qn = module_qn;
|
|
ctx->resolved_calls = out;
|
|
ctx->current_scope = cbm_scope_push(arena, NULL);
|
|
|
|
const char *dbg = getenv("CBM_LSP_DEBUG");
|
|
ctx->debug = (dbg && dbg[0]);
|
|
}
|
|
|
|
/* Doubling-array push of a `(local, full-path)` use entry. */
|
|
void rust_lsp_add_use(RustLSPContext *ctx, const char *local_name, const char *module_path) {
|
|
if (!ctx || !local_name || !module_path) {
|
|
return;
|
|
}
|
|
if (ctx->use_count % 32 == 0) {
|
|
int new_cap = ctx->use_count + 32;
|
|
const char **nl =
|
|
(const char **)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(char *));
|
|
const char **np =
|
|
(const char **)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(char *));
|
|
if (!nl || !np) {
|
|
return;
|
|
}
|
|
if (ctx->use_local_names && ctx->use_count > 0) {
|
|
memcpy(nl, ctx->use_local_names, ctx->use_count * sizeof(char *));
|
|
memcpy(np, ctx->use_module_paths, ctx->use_count * sizeof(char *));
|
|
}
|
|
ctx->use_local_names = nl;
|
|
ctx->use_module_paths = np;
|
|
}
|
|
ctx->use_local_names[ctx->use_count] = cbm_arena_strdup(ctx->arena, local_name);
|
|
ctx->use_module_paths[ctx->use_count] = cbm_arena_strdup(ctx->arena, module_path);
|
|
ctx->use_count++;
|
|
}
|
|
|
|
void rust_lsp_add_glob(RustLSPContext *ctx, const char *module_qn) {
|
|
if (!ctx || !module_qn) {
|
|
return;
|
|
}
|
|
if (ctx->glob_count % 16 == 0) {
|
|
int new_cap = ctx->glob_count + 16;
|
|
const char **ng =
|
|
(const char **)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(char *));
|
|
if (!ng) {
|
|
return;
|
|
}
|
|
if (ctx->glob_module_qns && ctx->glob_count > 0) {
|
|
memcpy(ng, ctx->glob_module_qns, ctx->glob_count * sizeof(char *));
|
|
}
|
|
ctx->glob_module_qns = ng;
|
|
}
|
|
ctx->glob_module_qns[ctx->glob_count++] = cbm_arena_strdup(ctx->arena, module_qn);
|
|
}
|
|
|
|
static char *rust_node_text(RustLSPContext *ctx, TSNode node) {
|
|
return cbm_node_text(ctx->arena, node, ctx->source);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 2. Builtin / prelude tables
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Rust primitive types that the grammar reports as `primitive_type`. */
|
|
static const char *RUST_PRIMITIVES[] = {"i8", "i16", "i32", "i64", "i128", "isize", "u8",
|
|
"u16", "u32", "u64", "u128", "usize", "f32", "f64",
|
|
"bool", "char", "str", "()", "!", NULL};
|
|
|
|
static bool is_rust_primitive(const char *name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
for (const char **p = RUST_PRIMITIVES; *p; p++) {
|
|
if (strcmp(*p, name) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Names of macros that behave like println-family: side effects only,
|
|
* return type `()`. */
|
|
static bool is_void_macro(const char *name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
static const char *m[] = {"println",
|
|
"print",
|
|
"eprintln",
|
|
"eprint",
|
|
"panic",
|
|
"unimplemented",
|
|
"todo",
|
|
"unreachable",
|
|
"assert",
|
|
"assert_eq",
|
|
"assert_ne",
|
|
"debug_assert",
|
|
"debug_assert_eq",
|
|
"debug_assert_ne",
|
|
"writeln",
|
|
"write",
|
|
NULL};
|
|
for (const char **p = m; *p; p++) {
|
|
if (strcmp(*p, name) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Names of macros that produce a `String` value. */
|
|
static bool is_string_macro(const char *name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
return strcmp(name, "format") == 0 || strcmp(name, "concat") == 0 ||
|
|
strcmp(name, "stringify") == 0 || strcmp(name, "env") == 0 ||
|
|
strcmp(name, "include_str") == 0;
|
|
}
|
|
|
|
/* Prelude trait names whose method short-names we treat as universally
|
|
* available (for emit-on-best-effort when we cannot pin down the trait
|
|
* impl). Borrowed from `core::prelude::v1`. */
|
|
static bool is_prelude_trait_method(const char *method_name) {
|
|
if (!method_name) {
|
|
return false;
|
|
}
|
|
static const char *m[] = {/* Clone / Copy / Default */
|
|
"clone", "default",
|
|
/* PartialEq / Eq / PartialOrd / Ord */
|
|
"eq", "ne", "cmp", "partial_cmp", "lt", "le", "gt", "ge",
|
|
/* Hash */
|
|
"hash",
|
|
/* Display / Debug */
|
|
"fmt", "to_string",
|
|
/* From / Into / TryFrom / TryInto */
|
|
"from", "into", "try_from", "try_into",
|
|
/* AsRef / AsMut / Borrow / BorrowMut */
|
|
"as_ref", "as_mut", "borrow", "borrow_mut",
|
|
/* Deref */
|
|
"deref", "deref_mut",
|
|
/* Drop */
|
|
"drop",
|
|
/* Iterator (most-used subset) */
|
|
"next", "iter", "iter_mut", "into_iter", "map", "filter", "fold",
|
|
"for_each", "collect", "count", "sum", "max", "min", "any", "all",
|
|
"find", "position", "enumerate", "zip", "chain", "take", "skip",
|
|
"rev", "cloned", "copied", "by_ref", "step_by", "flat_map", "flatten",
|
|
"filter_map", "peekable",
|
|
/* Future */
|
|
"poll", NULL};
|
|
for (const char **p = m; *p; p++) {
|
|
if (strcmp(*p, method_name) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 3. Path & use resolution
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Return the last `::`-separated segment of a Rust path (`std::io::Read` →
|
|
* `Read`). Pointer aliases into `path` — caller does not own. */
|
|
static const char *path_last_segment(const char *path) {
|
|
if (!path || !path[0]) {
|
|
return path;
|
|
}
|
|
const char *p = path;
|
|
const char *last = path;
|
|
while (*p) {
|
|
if (p[0] == ':' && p[1] == ':') {
|
|
last = p + 2;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
p++;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/* Convert a Rust path with `::` separators into our internal QN form using
|
|
* `.` separators. Always allocates a fresh string. */
|
|
static const char *convert_path_to_qn(CBMArena *arena, const char *path) {
|
|
if (!path || !path[0]) {
|
|
return path;
|
|
}
|
|
size_t len = strlen(path);
|
|
char *out = (char *)cbm_arena_alloc(arena, len + 1);
|
|
if (!out) {
|
|
return path;
|
|
}
|
|
size_t j = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (path[i] == ':' && i + 1 < len && path[i + 1] == ':') {
|
|
out[j++] = '.';
|
|
i++;
|
|
} else {
|
|
out[j++] = path[i];
|
|
}
|
|
}
|
|
out[j] = '\0';
|
|
return out;
|
|
}
|
|
|
|
/* In-place strip turbofish segments (`::<...>`) from a Rust path. The
|
|
* grammar exposes paths like `Vec::<i32>::new` or `parse::<u32>(s)` —
|
|
* the LSP cares about the underlying name, not the explicit type
|
|
* arguments, so we collapse `head::<args>::tail` to `head::tail`.
|
|
*
|
|
* Modifies `path` in place. Safe on NULL. */
|
|
static void rust_strip_turbofish(char *path) {
|
|
if (!path)
|
|
return;
|
|
char *read = path;
|
|
char *write = path;
|
|
while (*read) {
|
|
if (read[0] == ':' && read[1] == ':' && read[2] == '<') {
|
|
/* Skip ::< … > balanced. */
|
|
int depth = 1;
|
|
const char *p = read + 3;
|
|
while (*p && depth > 0) {
|
|
if (*p == '<')
|
|
depth++;
|
|
else if (*p == '>')
|
|
depth--;
|
|
p++;
|
|
}
|
|
read = (char *)p;
|
|
continue;
|
|
}
|
|
*write++ = *read++;
|
|
}
|
|
*write = '\0';
|
|
}
|
|
|
|
/* Look up a `use` alias and return its fully-qualified module path,
|
|
* or NULL if absent. The returned pointer aliases into the use map. */
|
|
static const char *rust_resolve_use(RustLSPContext *ctx, const char *local_name) {
|
|
if (!ctx || !local_name) {
|
|
return NULL;
|
|
}
|
|
for (int i = 0; i < ctx->use_count; i++) {
|
|
if (strcmp(ctx->use_local_names[i], local_name) == 0) {
|
|
return ctx->use_module_paths[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* The Rust prelude is auto-imported into every module. We map each name
|
|
* to its canonical QN so bare references (`String`, `Vec::new`, …)
|
|
* resolve without an explicit `use`. The list mirrors `core::prelude::v1`
|
|
* + `alloc::prelude` + `std::prelude::v1`. The mapping is consulted before
|
|
* the project-local fallback so prelude names always win. */
|
|
typedef struct {
|
|
const char *name;
|
|
const char *qn;
|
|
} RustPreludeEntry;
|
|
|
|
static const RustPreludeEntry RUST_PRELUDE[] = {{"String", "alloc.string.String"},
|
|
{"ToString", "alloc.string.ToString"},
|
|
{"Vec", "alloc.vec.Vec"},
|
|
{"VecDeque", "alloc.collections.VecDeque"},
|
|
{"HashMap", "alloc.collections.HashMap"},
|
|
{"BTreeMap", "alloc.collections.BTreeMap"},
|
|
{"HashSet", "alloc.collections.HashSet"},
|
|
{"BTreeSet", "alloc.collections.BTreeSet"},
|
|
{"Box", "alloc.boxed.Box"},
|
|
{"Rc", "alloc.rc.Rc"},
|
|
{"Arc", "alloc.sync.Arc"},
|
|
{"Option", "core.option.Option"},
|
|
{"Some", "core.option.Option.Some"},
|
|
{"None", "core.option.Option.None"},
|
|
{"Result", "core.result.Result"},
|
|
{"Ok", "core.result.Result.Ok"},
|
|
{"Err", "core.result.Result.Err"},
|
|
{"Iterator", "core.iter.Iterator"},
|
|
{"IntoIterator", "core.iter.IntoIterator"},
|
|
{"Future", "core.future.Future"},
|
|
{"Clone", "core.clone.Clone"},
|
|
{"Copy", "core.marker.Copy"},
|
|
{"Send", "core.marker.Send"},
|
|
{"Sync", "core.marker.Sync"},
|
|
{"Default", "core.default.Default"},
|
|
{"PartialEq", "core.cmp.PartialEq"},
|
|
{"Eq", "core.cmp.Eq"},
|
|
{"PartialOrd", "core.cmp.PartialOrd"},
|
|
{"Ord", "core.cmp.Ord"},
|
|
{"Hash", "core.hash.Hash"},
|
|
{"Display", "core.fmt.Display"},
|
|
{"Debug", "core.fmt.Debug"},
|
|
{"From", "core.convert.From"},
|
|
{"Into", "core.convert.Into"},
|
|
{"TryFrom", "core.convert.TryFrom"},
|
|
{"TryInto", "core.convert.TryInto"},
|
|
{"AsRef", "core.convert.AsRef"},
|
|
{"AsMut", "core.convert.AsMut"},
|
|
{"Borrow", "core.borrow.Borrow"},
|
|
{"BorrowMut", "core.borrow.BorrowMut"},
|
|
{"Deref", "core.ops.Deref"},
|
|
{"DerefMut", "core.ops.DerefMut"},
|
|
{"Drop", "core.ops.Drop"},
|
|
{"RefCell", "core.cell.RefCell"},
|
|
{"Cell", "core.cell.Cell"},
|
|
{"Mutex", "std.sync.Mutex"},
|
|
{"RwLock", "std.sync.RwLock"},
|
|
{NULL, NULL}};
|
|
|
|
static const char *rust_lookup_prelude(const char *name) {
|
|
if (!name)
|
|
return NULL;
|
|
for (const RustPreludeEntry *e = RUST_PRELUDE; e->name; e++) {
|
|
if (strcmp(e->name, name) == 0)
|
|
return e->qn;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Strip a leading `&` / `&mut` reference prefix from a textual type so we
|
|
* can compare the inner head segment against builtins. */
|
|
static const char *skip_ref_prefix(const char *text) {
|
|
if (!text) {
|
|
return text;
|
|
}
|
|
while (*text == '&' || isspace((unsigned char)*text)) {
|
|
text++;
|
|
}
|
|
if (strncmp(text, "mut ", 4) == 0) {
|
|
text += 4;
|
|
while (isspace((unsigned char)*text)) {
|
|
text++;
|
|
}
|
|
}
|
|
/* Also strip a single explicit lifetime: `'a ` */
|
|
if (*text == '\'') {
|
|
text++;
|
|
while (*text && (isalnum((unsigned char)*text) || *text == '_')) {
|
|
text++;
|
|
}
|
|
while (isspace((unsigned char)*text)) {
|
|
text++;
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/* Resolve a Rust *path expression* (e.g. `Foo::bar` or `crate::x::y`)
|
|
* into a canonical QN. The resolver cascades through these rules,
|
|
* matching what `rust-analyzer`'s name resolver does at the path level:
|
|
*
|
|
* 1. `Self::X` → `<self_type_qn>.X`
|
|
* 2. `crate::a::b` → `<root_module_qn>.a.b`
|
|
* 3. `super::a` → strip last segment of `module_qn` and prepend
|
|
* 4. Single-segment + matches a `use` local-name → `<full path>.X`
|
|
* 5. Multi-segment whose first segment is a `use` local → splice
|
|
* 6. Falls through unchanged (caller decides what to do).
|
|
*
|
|
* The returned string is arena-owned; in case (6) we return the input
|
|
* with `::` already converted to `.`. */
|
|
static const char *rust_resolve_path_expr(RustLSPContext *ctx, const char *path) {
|
|
if (!ctx || !path || !path[0]) {
|
|
return path;
|
|
}
|
|
|
|
/* Self:: handling — we treat the receiver type's QN as the head. */
|
|
if (strncmp(path, "Self::", 6) == 0 && ctx->self_type_qn) {
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->self_type_qn,
|
|
convert_path_to_qn(ctx->arena, path + 6));
|
|
}
|
|
if (strcmp(path, "Self") == 0 && ctx->self_type_qn) {
|
|
return ctx->self_type_qn;
|
|
}
|
|
|
|
/* crate:: → <root>. We approximate the crate root as the first dotted
|
|
* segment of `module_qn` after the project prefix. The pipeline
|
|
* forms `module_qn` as `<project>.<crate>.<rel-path-segments>`, so
|
|
* the first two segments are project + crate root. */
|
|
if (strncmp(path, "crate::", 7) == 0 && ctx->module_qn) {
|
|
const char *p = ctx->module_qn;
|
|
int dots = 0;
|
|
const char *second_dot = NULL;
|
|
for (; *p; p++) {
|
|
if (*p == '.') {
|
|
if (++dots == 2) {
|
|
second_dot = p;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
size_t crate_len =
|
|
second_dot ? (size_t)(second_dot - ctx->module_qn) : strlen(ctx->module_qn);
|
|
char *crate_buf = cbm_arena_strndup(ctx->arena, ctx->module_qn, crate_len);
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", crate_buf,
|
|
convert_path_to_qn(ctx->arena, path + 7));
|
|
}
|
|
|
|
/* super:: → drop last segment of module_qn. */
|
|
if (strncmp(path, "super::", 7) == 0 && ctx->module_qn) {
|
|
const char *dot = strrchr(ctx->module_qn, '.');
|
|
if (dot) {
|
|
char *parent =
|
|
cbm_arena_strndup(ctx->arena, ctx->module_qn, (size_t)(dot - ctx->module_qn));
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", parent,
|
|
convert_path_to_qn(ctx->arena, path + 7));
|
|
}
|
|
}
|
|
|
|
/* Find first "::" — split into head + tail. */
|
|
const char *sep = strstr(path, "::");
|
|
if (!sep) {
|
|
const char *full = rust_resolve_use(ctx, path);
|
|
if (full) {
|
|
return convert_path_to_qn(ctx->arena, full);
|
|
}
|
|
/* Prelude name (e.g. `String`, `Vec`)? */
|
|
const char *prelude = rust_lookup_prelude(path);
|
|
if (prelude) {
|
|
return prelude;
|
|
}
|
|
/* Bare identifier — assume same module. */
|
|
if (ctx->module_qn) {
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, path);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
char *head = cbm_arena_strndup(ctx->arena, path, (size_t)(sep - path));
|
|
const char *tail = sep + 2;
|
|
|
|
const char *full = rust_resolve_use(ctx, head);
|
|
if (full) {
|
|
/* The use-map's full path already includes `head` as the last
|
|
* segment; concat its parent with the rest. */
|
|
const char *full_dotted = convert_path_to_qn(ctx->arena, full);
|
|
const char *tail_dotted = convert_path_to_qn(ctx->arena, tail);
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", full_dotted, tail_dotted);
|
|
}
|
|
|
|
/* Prelude head: `String::from` → `alloc.string.String.from`. */
|
|
const char *prelude = rust_lookup_prelude(head);
|
|
if (prelude) {
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", prelude,
|
|
convert_path_to_qn(ctx->arena, tail));
|
|
}
|
|
|
|
/* Cargo-manifest aware routing — when a Cargo.toml has been parsed
|
|
* and the path head matches either a declared dependency or a
|
|
* workspace member, return the canonical form `<head>.<tail>` so
|
|
* the resolver doesn't pollute the module-prefix space. */
|
|
if (ctx->cargo_manifest) {
|
|
const CBMCargoManifest *m = (const CBMCargoManifest *)ctx->cargo_manifest;
|
|
const CBMCargoMember *mem = cbm_cargo_find_member(m, head);
|
|
if (mem) {
|
|
/* Workspace member: route to `<member_name>.<tail>` so the
|
|
* pipeline's cross-crate resolution can match it. */
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", head,
|
|
convert_path_to_qn(ctx->arena, tail));
|
|
}
|
|
if (cbm_cargo_is_known_dep(m, head)) {
|
|
/* Declared dependency: same canonical form. The actual
|
|
* methods may have been pre-seeded by rust_crates_seed.c;
|
|
* otherwise the call is correctly attributed to an
|
|
* external crate rather than fabricated locally. */
|
|
return cbm_arena_sprintf(ctx->arena, "%s.%s", head,
|
|
convert_path_to_qn(ctx->arena, tail));
|
|
}
|
|
}
|
|
|
|
/* Treat unknown-head paths as absolute: `std::io::Read` → `std.io.Read`. */
|
|
return convert_path_to_qn(ctx->arena, path);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 4. Type-AST → CBMType
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Reconstruct the textual Rust path under a `scoped_type_identifier` /
|
|
* `scoped_identifier` node. We deliberately walk the named children
|
|
* rather than using the literal source text so we do not preserve
|
|
* whitespace or trailing turbofish noise. */
|
|
static char *gather_scoped_path(RustLSPContext *ctx, TSNode node) {
|
|
/* Fall back to the raw source text — the grammar already produces a
|
|
* tight `path::to::name` literal under the node. */
|
|
return rust_node_text(ctx, node);
|
|
}
|
|
|
|
/* Resolve a textual Rust path (with `::`) into a registered type's QN, or
|
|
* NULL if no match. */
|
|
static const char *resolve_path_to_type_qn(RustLSPContext *ctx, const char *path) {
|
|
if (!ctx || !path || !path[0]) {
|
|
return NULL;
|
|
}
|
|
if (is_rust_primitive(path)) {
|
|
return NULL;
|
|
}
|
|
const char *qn = rust_resolve_path_expr(ctx, path);
|
|
if (!qn) {
|
|
return NULL;
|
|
}
|
|
if (cbm_registry_lookup_type(ctx->registry, qn)) {
|
|
return qn;
|
|
}
|
|
return qn; /* may not be registered yet but caller can still wrap as NAMED */
|
|
}
|
|
|
|
const CBMType *rust_parse_type_node(RustLSPContext *ctx, TSNode node) {
|
|
if (ts_node_is_null(node)) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *kind = ts_node_type(node);
|
|
|
|
/* primitive_type: i32, bool, char, str, … */
|
|
if (strcmp(kind, "primitive_type") == 0) {
|
|
char *name = rust_node_text(ctx, node);
|
|
if (!name) {
|
|
return cbm_type_unknown();
|
|
}
|
|
return cbm_type_builtin(ctx->arena, name);
|
|
}
|
|
|
|
/* type_identifier: simple named type */
|
|
if (strcmp(kind, "type_identifier") == 0) {
|
|
char *name = rust_node_text(ctx, node);
|
|
if (!name) {
|
|
return cbm_type_unknown();
|
|
}
|
|
if (is_rust_primitive(name)) {
|
|
return cbm_type_builtin(ctx->arena, name);
|
|
}
|
|
if (strcmp(name, "Self") == 0 && ctx->self_type_qn) {
|
|
return cbm_type_named(ctx->arena, ctx->self_type_qn);
|
|
}
|
|
const char *qn = rust_resolve_path_expr(ctx, name);
|
|
return cbm_type_named(ctx->arena, qn);
|
|
}
|
|
|
|
/* scoped_type_identifier: A::B::C */
|
|
if (strcmp(kind, "scoped_type_identifier") == 0) {
|
|
char *path = gather_scoped_path(ctx, node);
|
|
if (!path) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *qn = rust_resolve_path_expr(ctx, path);
|
|
return cbm_type_named(ctx->arena, qn);
|
|
}
|
|
|
|
/* reference_type: &T or &mut T */
|
|
if (strcmp(kind, "reference_type") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(node, "type", 4);
|
|
if (ts_node_is_null(inner)) {
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
if (nc > 0) {
|
|
inner = ts_node_named_child(node, nc - 1);
|
|
}
|
|
}
|
|
const CBMType *elem = rust_parse_type_node(ctx, inner);
|
|
return cbm_type_reference(ctx->arena, elem);
|
|
}
|
|
|
|
/* pointer_type: *const T / *mut T */
|
|
if (strcmp(kind, "pointer_type") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(node, "type", 4);
|
|
if (ts_node_is_null(inner)) {
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
if (nc > 0) {
|
|
inner = ts_node_named_child(node, nc - 1);
|
|
}
|
|
}
|
|
return cbm_type_pointer(ctx->arena, rust_parse_type_node(ctx, inner));
|
|
}
|
|
|
|
/* array_type: [T; N] — treated as slice T */
|
|
if (strcmp(kind, "array_type") == 0) {
|
|
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
|
|
if (ts_node_is_null(elem)) {
|
|
elem = ts_node_child_by_field_name(node, "type", 4);
|
|
}
|
|
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0) {
|
|
elem = ts_node_named_child(node, 0);
|
|
}
|
|
return cbm_type_slice(ctx->arena, rust_parse_type_node(ctx, elem));
|
|
}
|
|
|
|
/* slice_type: [T] */
|
|
if (strcmp(kind, "slice_type") == 0) {
|
|
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
|
|
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0) {
|
|
elem = ts_node_named_child(node, 0);
|
|
}
|
|
return cbm_type_slice(ctx->arena, rust_parse_type_node(ctx, elem));
|
|
}
|
|
|
|
/* tuple_type: (T1, T2, …) */
|
|
if (strcmp(kind, "tuple_type") == 0) {
|
|
const CBMType *elems[16];
|
|
int count = 0;
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < nc && count < 16; i++) {
|
|
elems[count++] = rust_parse_type_node(ctx, ts_node_named_child(node, i));
|
|
}
|
|
if (count == 0) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
if (count == 1) {
|
|
return elems[0];
|
|
}
|
|
return cbm_type_tuple(ctx->arena, elems, count);
|
|
}
|
|
|
|
/* unit_type: () */
|
|
if (strcmp(kind, "unit_type") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
|
|
/* never_type: ! */
|
|
if (strcmp(kind, "never_type") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "!");
|
|
}
|
|
|
|
/* generic_type: Foo<T1, T2, …> */
|
|
if (strcmp(kind, "generic_type") == 0) {
|
|
TSNode tn = ts_node_child_by_field_name(node, "type", 4);
|
|
if (ts_node_is_null(tn) && ts_node_named_child_count(node) > 0) {
|
|
tn = ts_node_named_child(node, 0);
|
|
}
|
|
char *head = rust_node_text(ctx, tn);
|
|
if (!head) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *head_qn = rust_resolve_path_expr(ctx, head);
|
|
|
|
/* Gather type_arguments. */
|
|
TSNode args = ts_node_child_by_field_name(node, "type_arguments", 14);
|
|
const CBMType *targs[16];
|
|
int targ_count = 0;
|
|
if (!ts_node_is_null(args)) {
|
|
uint32_t anc = ts_node_named_child_count(args);
|
|
for (uint32_t i = 0; i < anc && targ_count < 15; i++) {
|
|
TSNode tc = ts_node_named_child(args, i);
|
|
const char *tk = ts_node_type(tc);
|
|
/* Skip lifetime arguments — we ignore lifetimes entirely. */
|
|
if (strcmp(tk, "lifetime") == 0) {
|
|
continue;
|
|
}
|
|
targs[targ_count++] = rust_parse_type_node(ctx, tc);
|
|
}
|
|
}
|
|
if (targ_count > 0) {
|
|
return cbm_type_template(ctx->arena, head_qn, targs, targ_count);
|
|
}
|
|
return cbm_type_named(ctx->arena, head_qn);
|
|
}
|
|
|
|
/* function_type: fn(T1, T2) -> R */
|
|
if (strcmp(kind, "function_type") == 0) {
|
|
return cbm_type_func(ctx->arena, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* dynamic_type: dyn Trait — record as named on the trait QN */
|
|
if (strcmp(kind, "dynamic_type") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(node, "trait", 5);
|
|
if (ts_node_is_null(inner) && ts_node_named_child_count(node) > 0) {
|
|
inner = ts_node_named_child(node, 0);
|
|
}
|
|
return rust_parse_type_node(ctx, inner);
|
|
}
|
|
|
|
/* abstract_type: impl Trait — best-effort same as dyn Trait */
|
|
if (strcmp(kind, "abstract_type") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(node, "trait", 5);
|
|
if (ts_node_is_null(inner) && ts_node_named_child_count(node) > 0) {
|
|
inner = ts_node_named_child(node, 0);
|
|
}
|
|
return rust_parse_type_node(ctx, inner);
|
|
}
|
|
|
|
/* bounded_type: T + Trait + 'a — take the first child */
|
|
if (strcmp(kind, "bounded_type") == 0 && ts_node_named_child_count(node) > 0) {
|
|
return rust_parse_type_node(ctx, ts_node_named_child(node, 0));
|
|
}
|
|
|
|
/* parenthesized_type or wrapped types */
|
|
if (strcmp(kind, "parenthesized_type") == 0 && ts_node_named_child_count(node) > 0) {
|
|
return rust_parse_type_node(ctx, ts_node_named_child(node, 0));
|
|
}
|
|
|
|
/* qualified_type: <T as Trait>::Item */
|
|
if (strcmp(kind, "qualified_type") == 0) {
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* Parse a textual Rust type (`Vec<String>`, `&mut Foo`, `Result<T, E>`)
|
|
* into a CBMType. Used when we receive types as strings (return types of
|
|
* extracted `CBMDefinition`s, cross-file `CBMRustLSPDef::return_types`,
|
|
* stdlib seed entries).
|
|
*
|
|
* The parser is intentionally simple: it recognises the small surface
|
|
* area that tree-sitter would produce in `rust_parse_type_node` but
|
|
* without a parser. This is the same trade-off `cbm_rust_parse_return_type_text`
|
|
* makes for Go. */
|
|
static const CBMType *parse_type_text_with_params(CBMArena *arena, const char *text,
|
|
const char *module_qn, const char **type_params) {
|
|
if (!text || !text[0]) {
|
|
return cbm_type_unknown();
|
|
}
|
|
/* Skip leading whitespace + lifetime + mut markers. */
|
|
while (*text == ' ' || *text == '\t') {
|
|
text++;
|
|
}
|
|
|
|
/* HRTB: `for<'a, 'b> Fn(&'a T) -> R` — strip the higher-rank
|
|
* binder. We don't reason about explicit lifetimes anywhere, so
|
|
* dropping it leaves the rest of the type untouched. */
|
|
if (strncmp(text, "for<", 4) == 0) {
|
|
const char *p = text + 4;
|
|
int depth = 1;
|
|
while (*p && depth > 0) {
|
|
if (*p == '<')
|
|
depth++;
|
|
else if (*p == '>')
|
|
depth--;
|
|
p++;
|
|
}
|
|
while (*p == ' ')
|
|
p++;
|
|
text = p;
|
|
if (!*text)
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* Bare leading lifetime (e.g. `'a`) — accept and skip, treating
|
|
* the rest of the text as the actual type. Rare outside of HRTBs
|
|
* but cheap to handle. */
|
|
if (text[0] == '\'' && (isalpha((unsigned char)text[1]) || text[1] == '_')) {
|
|
const char *p = text + 1;
|
|
while (*p && (isalnum((unsigned char)*p) || *p == '_'))
|
|
p++;
|
|
while (*p == ' ')
|
|
p++;
|
|
text = p;
|
|
if (!*text)
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* Reference: &T or &'a T or &mut T or &'a mut T */
|
|
if (text[0] == '&') {
|
|
const char *p = text + 1;
|
|
if (*p == '\'') {
|
|
p++;
|
|
while (*p && (isalnum((unsigned char)*p) || *p == '_')) {
|
|
p++;
|
|
}
|
|
}
|
|
while (*p == ' ') {
|
|
p++;
|
|
}
|
|
if (strncmp(p, "mut ", 4) == 0) {
|
|
p += 4;
|
|
}
|
|
const CBMType *elem = parse_type_text_with_params(arena, p, module_qn, type_params);
|
|
return cbm_type_reference(arena, elem);
|
|
}
|
|
|
|
/* Pointer: *const T / *mut T */
|
|
if (text[0] == '*') {
|
|
const char *p = text + 1;
|
|
if (strncmp(p, "const ", 6) == 0) {
|
|
p += 6;
|
|
} else if (strncmp(p, "mut ", 4) == 0) {
|
|
p += 4;
|
|
}
|
|
return cbm_type_pointer(arena,
|
|
parse_type_text_with_params(arena, p, module_qn, type_params));
|
|
}
|
|
|
|
/* Slice: [T] */
|
|
if (text[0] == '[' && text[strlen(text) - 1] == ']') {
|
|
const char *p = text + 1;
|
|
const char *end = text + strlen(text) - 1;
|
|
size_t inner_len = (size_t)(end - p);
|
|
char *inner = cbm_arena_strndup(arena, p, inner_len);
|
|
/* Array form `[T; N]` — strip the count. */
|
|
char *semi = strchr(inner, ';');
|
|
if (semi) {
|
|
*semi = '\0';
|
|
/* Trim trailing whitespace. */
|
|
char *q = semi - 1;
|
|
while (q > inner && isspace((unsigned char)*q)) {
|
|
*q-- = '\0';
|
|
}
|
|
}
|
|
return cbm_type_slice(arena,
|
|
parse_type_text_with_params(arena, inner, module_qn, type_params));
|
|
}
|
|
|
|
/* Unit / never */
|
|
if (strcmp(text, "()") == 0) {
|
|
return cbm_type_builtin(arena, "()");
|
|
}
|
|
if (strcmp(text, "!") == 0) {
|
|
return cbm_type_builtin(arena, "!");
|
|
}
|
|
|
|
/* Tuple: (T1, T2, …) — only when not a single parenthesised type. */
|
|
if (text[0] == '(' && text[strlen(text) - 1] == ')') {
|
|
const char *p = text + 1;
|
|
size_t inner_len = strlen(text) - 2;
|
|
char *inner = cbm_arena_strndup(arena, p, inner_len);
|
|
/* Detect comma at top level. */
|
|
int depth = 0;
|
|
bool has_comma = false;
|
|
for (char *q = inner; *q; q++) {
|
|
if (*q == '<' || *q == '(' || *q == '[')
|
|
depth++;
|
|
else if (*q == '>' || *q == ')' || *q == ']')
|
|
depth--;
|
|
else if (*q == ',' && depth == 0) {
|
|
has_comma = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_comma) {
|
|
return parse_type_text_with_params(arena, inner, module_qn, type_params);
|
|
}
|
|
/* Split by top-level commas. */
|
|
const CBMType *elems[16];
|
|
int count = 0;
|
|
char *start = inner;
|
|
depth = 0;
|
|
for (char *q = inner;; q++) {
|
|
if (*q == '<' || *q == '(' || *q == '[')
|
|
depth++;
|
|
else if (*q == '>' || *q == ')' || *q == ']')
|
|
depth--;
|
|
if ((*q == ',' && depth == 0) || *q == '\0') {
|
|
char save = *q;
|
|
*q = '\0';
|
|
/* Trim. */
|
|
while (*start == ' ')
|
|
start++;
|
|
if (count < 15 && *start) {
|
|
elems[count++] =
|
|
parse_type_text_with_params(arena, start, module_qn, type_params);
|
|
}
|
|
if (save == '\0')
|
|
break;
|
|
start = q + 1;
|
|
}
|
|
}
|
|
if (count == 0)
|
|
return cbm_type_builtin(arena, "()");
|
|
if (count == 1)
|
|
return elems[0];
|
|
return cbm_type_tuple(arena, elems, count);
|
|
}
|
|
|
|
/* Generic head: head<args> */
|
|
const char *lt = strchr(text, '<');
|
|
if (lt) {
|
|
const char *gt = text + strlen(text) - 1;
|
|
if (*gt == '>') {
|
|
size_t head_len = (size_t)(lt - text);
|
|
char *head = cbm_arena_strndup(arena, text, head_len);
|
|
/* Recursive split of args by top-level commas. */
|
|
const char *args = lt + 1;
|
|
size_t args_len = (size_t)(gt - args);
|
|
char *abuf = cbm_arena_strndup(arena, args, args_len);
|
|
const CBMType *targs[16];
|
|
int targ_count = 0;
|
|
int depth = 0;
|
|
char *start = abuf;
|
|
for (char *q = abuf;; q++) {
|
|
if (*q == '<' || *q == '(' || *q == '[')
|
|
depth++;
|
|
else if (*q == '>' || *q == ')' || *q == ']')
|
|
depth--;
|
|
if ((*q == ',' && depth == 0) || *q == '\0') {
|
|
char save = *q;
|
|
*q = '\0';
|
|
while (*start == ' ')
|
|
start++;
|
|
/* Skip lifetime args. */
|
|
if (*start != '\'' && *start && targ_count < 15) {
|
|
targs[targ_count++] =
|
|
parse_type_text_with_params(arena, start, module_qn, type_params);
|
|
}
|
|
if (save == '\0')
|
|
break;
|
|
start = q + 1;
|
|
}
|
|
}
|
|
const char *head_qn = head;
|
|
if (is_rust_primitive(head)) {
|
|
/* Primitives don't take generics in practice except for str ref — pass through. */
|
|
return cbm_type_builtin(arena, head);
|
|
}
|
|
/* Map a few well-known std type sugars. */
|
|
return cbm_type_template(arena, head_qn, targs, targ_count);
|
|
}
|
|
}
|
|
|
|
/* Bare identifier or path. */
|
|
if (is_rust_primitive(text)) {
|
|
return cbm_type_builtin(arena, text);
|
|
}
|
|
if (type_params) {
|
|
for (int i = 0; type_params[i]; i++) {
|
|
if (strcmp(text, type_params[i]) == 0) {
|
|
return cbm_type_type_param(arena, text);
|
|
}
|
|
}
|
|
}
|
|
/* Self -> module-qualified placeholder caller will substitute. */
|
|
if (strcmp(text, "Self") == 0) {
|
|
return cbm_type_named(arena, "Self");
|
|
}
|
|
/* Has `::` → absolute path; treat dotted paths as already-qualified
|
|
* QNs (cross-file callers pass module-qualified text directly). */
|
|
if (strstr(text, "::")) {
|
|
return cbm_type_named(arena, convert_path_to_qn(arena, text));
|
|
}
|
|
if (strchr(text, '.')) {
|
|
return cbm_type_named(arena, text);
|
|
}
|
|
return cbm_type_named(arena, cbm_arena_sprintf(arena, "%s.%s", module_qn, text));
|
|
}
|
|
|
|
/* Public-ish helper used by the cross-file path. */
|
|
static const CBMType *rust_parse_return_type_text(CBMArena *arena, const char *text,
|
|
const char *module_qn) {
|
|
return parse_type_text_with_params(arena, text, module_qn, NULL);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 5. Generic substitution
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Recursively substitute every `TYPE_PARAM` reference in `t` whose name
|
|
* matches `params[i]` with `args[i]`. Preserves structure for composite
|
|
* types. */
|
|
static const CBMType *rust_substitute_type(CBMArena *arena, const CBMType *t, const char **params,
|
|
const CBMType **args) {
|
|
if (!t || !params || !args) {
|
|
return t;
|
|
}
|
|
switch (t->kind) {
|
|
case CBM_TYPE_TYPE_PARAM:
|
|
for (int i = 0; params[i]; i++) {
|
|
if (strcmp(t->data.type_param.name, params[i]) == 0) {
|
|
return args[i];
|
|
}
|
|
}
|
|
return t;
|
|
case CBM_TYPE_REFERENCE:
|
|
return cbm_type_reference(
|
|
arena, rust_substitute_type(arena, t->data.reference.elem, params, args));
|
|
case CBM_TYPE_POINTER:
|
|
return cbm_type_pointer(arena,
|
|
rust_substitute_type(arena, t->data.pointer.elem, params, args));
|
|
case CBM_TYPE_SLICE:
|
|
return cbm_type_slice(arena, rust_substitute_type(arena, t->data.slice.elem, params, args));
|
|
case CBM_TYPE_TEMPLATE: {
|
|
const CBMType *new_args[16];
|
|
int n = 0;
|
|
for (; n < t->data.template_type.arg_count && n < 16; n++) {
|
|
new_args[n] =
|
|
rust_substitute_type(arena, t->data.template_type.template_args[n], params, args);
|
|
}
|
|
return cbm_type_template(arena, t->data.template_type.template_name, new_args, n);
|
|
}
|
|
case CBM_TYPE_TUPLE: {
|
|
const CBMType *new_elems[16];
|
|
int n = 0;
|
|
for (; n < t->data.tuple.count && n < 16; n++) {
|
|
new_elems[n] = rust_substitute_type(arena, t->data.tuple.elems[n], params, args);
|
|
}
|
|
return cbm_type_tuple(arena, new_elems, n);
|
|
}
|
|
default:
|
|
return t;
|
|
}
|
|
}
|
|
|
|
/* Naive Hindley-Milner-style type unification. Walks `param_type`
|
|
* structurally against `arg_type`; whenever a `TYPE_PARAM` is bound for
|
|
* the first time, store the corresponding `arg_type`. Subsequent
|
|
* conflicting bindings are ignored (best-effort). */
|
|
static void rust_unify_type(const CBMType *param_type, const CBMType *arg_type,
|
|
const char **type_param_names, const CBMType **inferred,
|
|
int param_count) {
|
|
if (!param_type || !arg_type || cbm_type_is_unknown(arg_type)) {
|
|
return;
|
|
}
|
|
if (param_type->kind == CBM_TYPE_TYPE_PARAM) {
|
|
for (int i = 0; i < param_count; i++) {
|
|
if (strcmp(param_type->data.type_param.name, type_param_names[i]) == 0) {
|
|
if (!inferred[i]) {
|
|
inferred[i] = arg_type;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (param_type->kind == CBM_TYPE_REFERENCE && arg_type->kind == CBM_TYPE_REFERENCE) {
|
|
rust_unify_type(param_type->data.reference.elem, arg_type->data.reference.elem,
|
|
type_param_names, inferred, param_count);
|
|
return;
|
|
}
|
|
if (param_type->kind == CBM_TYPE_REFERENCE) {
|
|
rust_unify_type(param_type->data.reference.elem, arg_type, type_param_names, inferred,
|
|
param_count);
|
|
return;
|
|
}
|
|
if (param_type->kind == CBM_TYPE_SLICE && arg_type->kind == CBM_TYPE_SLICE) {
|
|
rust_unify_type(param_type->data.slice.elem, arg_type->data.slice.elem, type_param_names,
|
|
inferred, param_count);
|
|
return;
|
|
}
|
|
if (param_type->kind == CBM_TYPE_TEMPLATE && arg_type->kind == CBM_TYPE_TEMPLATE) {
|
|
if (param_type->data.template_type.arg_count == arg_type->data.template_type.arg_count) {
|
|
int ac = param_type->data.template_type.arg_count;
|
|
for (int i = 0; i < ac; i++) {
|
|
rust_unify_type(param_type->data.template_type.template_args[i],
|
|
arg_type->data.template_type.template_args[i], type_param_names,
|
|
inferred, param_count);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
/* TUPLE unification — needed for tuple-return generics like
|
|
* `fn pair<A, B>(a: A, b: B) -> (A, B)`. */
|
|
if (param_type->kind == CBM_TYPE_TUPLE && arg_type->kind == CBM_TYPE_TUPLE) {
|
|
int pc = param_type->data.tuple.count;
|
|
int ac = arg_type->data.tuple.count;
|
|
int min_ = pc < ac ? pc : ac;
|
|
for (int i = 0; i < min_; i++) {
|
|
rust_unify_type(param_type->data.tuple.elems[i], arg_type->data.tuple.elems[i],
|
|
type_param_names, inferred, param_count);
|
|
}
|
|
return;
|
|
}
|
|
/* POINTER unification. */
|
|
if (param_type->kind == CBM_TYPE_POINTER && arg_type->kind == CBM_TYPE_POINTER) {
|
|
rust_unify_type(param_type->data.pointer.elem, arg_type->data.pointer.elem,
|
|
type_param_names, inferred, param_count);
|
|
return;
|
|
}
|
|
/* Bidirectional fallback: if `arg_type` (rather than `param_type`)
|
|
* carries the type-param marker, swap and retry. This lets
|
|
* `unify(known_concrete, fresh_var)` solve the var. */
|
|
if (arg_type->kind == CBM_TYPE_TYPE_PARAM) {
|
|
rust_unify_type(arg_type, param_type, type_param_names, inferred, param_count);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Apply a solved type-param environment to a type, recursively
|
|
* substituting bound param names with their concrete types. Returns
|
|
* the substituted type (arena-allocated when new structure is built).
|
|
*
|
|
* This is the post-solve step of HM-lite: after `rust_unify_type` has
|
|
* filled the `inferred` array, this helper walks a target type and
|
|
* rewrites every `TYPE_PARAM` reference. */
|
|
static const CBMType *rust_apply_subst(CBMArena *arena, const CBMType *t, const char **names,
|
|
const CBMType **inferred, int count) {
|
|
if (!t)
|
|
return t;
|
|
switch (t->kind) {
|
|
case CBM_TYPE_TYPE_PARAM:
|
|
for (int i = 0; i < count; i++) {
|
|
if (inferred[i] && names[i] && strcmp(t->data.type_param.name, names[i]) == 0) {
|
|
return inferred[i];
|
|
}
|
|
}
|
|
return t;
|
|
case CBM_TYPE_REFERENCE:
|
|
return cbm_type_reference(
|
|
arena, rust_apply_subst(arena, t->data.reference.elem, names, inferred, count));
|
|
case CBM_TYPE_POINTER:
|
|
return cbm_type_pointer(
|
|
arena, rust_apply_subst(arena, t->data.pointer.elem, names, inferred, count));
|
|
case CBM_TYPE_SLICE:
|
|
return cbm_type_slice(arena,
|
|
rust_apply_subst(arena, t->data.slice.elem, names, inferred, count));
|
|
case CBM_TYPE_TEMPLATE: {
|
|
int n = t->data.template_type.arg_count;
|
|
if (n <= 0)
|
|
return t;
|
|
const CBMType *new_args[16];
|
|
if (n > 16)
|
|
n = 16;
|
|
for (int i = 0; i < n; i++) {
|
|
new_args[i] = rust_apply_subst(arena, t->data.template_type.template_args[i], names,
|
|
inferred, count);
|
|
}
|
|
return cbm_type_template(arena, t->data.template_type.template_name, new_args, n);
|
|
}
|
|
case CBM_TYPE_TUPLE: {
|
|
int n = t->data.tuple.count;
|
|
if (n <= 0)
|
|
return t;
|
|
const CBMType *new_elems[16];
|
|
if (n > 16)
|
|
n = 16;
|
|
for (int i = 0; i < n; i++) {
|
|
new_elems[i] = rust_apply_subst(arena, t->data.tuple.elems[i], names, inferred, count);
|
|
}
|
|
return cbm_type_tuple(arena, new_elems, n);
|
|
}
|
|
default:
|
|
return t;
|
|
}
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 6. Expression evaluator
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Evaluate the type of a literal child like `integer_literal`, `float_literal`,
|
|
* `string_literal`, `char_literal`, `boolean_literal`. */
|
|
static const CBMType *rust_eval_literal_type(RustLSPContext *ctx, const char *kind, TSNode node) {
|
|
if (strcmp(kind, "integer_literal") == 0) {
|
|
char *text = rust_node_text(ctx, node);
|
|
/* Look at suffix. */
|
|
if (text) {
|
|
/* Strip leading `-` if any. */
|
|
const char *p = text;
|
|
if (*p == '-')
|
|
p++;
|
|
/* Find suffix start (first non-digit/non-_/non-x/non-X/non-hex). */
|
|
while (*p && (isdigit((unsigned char)*p) || *p == '_' || *p == '.' || *p == 'x' ||
|
|
*p == 'X' || *p == 'b' || *p == 'B' || *p == 'o' || *p == 'O' ||
|
|
(*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) {
|
|
p++;
|
|
}
|
|
if (*p) {
|
|
return cbm_type_builtin(ctx->arena, p);
|
|
}
|
|
}
|
|
return cbm_type_builtin(ctx->arena, "i32");
|
|
}
|
|
if (strcmp(kind, "float_literal") == 0) {
|
|
char *text = rust_node_text(ctx, node);
|
|
if (text) {
|
|
const char *p = text;
|
|
while (*p && (isdigit((unsigned char)*p) || *p == '.' || *p == 'e' || *p == 'E' ||
|
|
*p == '+' || *p == '-' || *p == '_')) {
|
|
p++;
|
|
}
|
|
if (*p) {
|
|
return cbm_type_builtin(ctx->arena, p);
|
|
}
|
|
}
|
|
return cbm_type_builtin(ctx->arena, "f64");
|
|
}
|
|
if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "raw_string_literal") == 0) {
|
|
/* &'static str — represented as &str */
|
|
return cbm_type_reference(ctx->arena, cbm_type_builtin(ctx->arena, "str"));
|
|
}
|
|
if (strcmp(kind, "char_literal") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "char");
|
|
}
|
|
if (strcmp(kind, "boolean_literal") == 0 || strcmp(kind, "true") == 0 ||
|
|
strcmp(kind, "false") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* Look up the registered method or field type for a field/method-style
|
|
* access. Order: inherent method → field → trait method (with single-impl
|
|
* preference). */
|
|
static const CBMType *rust_eval_member_access(RustLSPContext *ctx, const CBMType *recv,
|
|
const char *member);
|
|
|
|
const CBMType *rust_eval_expr_type(RustLSPContext *ctx, TSNode node) {
|
|
if (ts_node_is_null(node)) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *kind = ts_node_type(node);
|
|
|
|
/* Identifier: scope or registered symbol. */
|
|
if (strcmp(kind, "identifier") == 0) {
|
|
char *name = rust_node_text(ctx, node);
|
|
if (!name) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const CBMType *t = cbm_scope_lookup(ctx->current_scope, name);
|
|
if (!cbm_type_is_unknown(t)) {
|
|
return t;
|
|
}
|
|
/* Module-level function. */
|
|
const CBMRegisteredFunc *f =
|
|
cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name);
|
|
if (f && f->signature) {
|
|
return f->signature;
|
|
}
|
|
/* Use-aliased symbol: resolve path then look up. */
|
|
const char *full = rust_resolve_use(ctx, name);
|
|
if (full) {
|
|
const char *qn = convert_path_to_qn(ctx->arena, full);
|
|
const CBMRegisteredFunc *uf = cbm_registry_lookup_func(ctx->registry, qn);
|
|
if (uf && uf->signature) {
|
|
return uf->signature;
|
|
}
|
|
const CBMRegisteredType *ut = cbm_registry_lookup_type(ctx->registry, qn);
|
|
if (ut) {
|
|
return cbm_type_named(ctx->arena, qn);
|
|
}
|
|
}
|
|
/* Same-module type? */
|
|
const char *type_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name);
|
|
if (cbm_registry_lookup_type(ctx->registry, type_qn)) {
|
|
return cbm_type_named(ctx->arena, type_qn);
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* self_parameter token: bound in scope as `self`. */
|
|
if (strcmp(kind, "self") == 0 || strcmp(kind, "self_parameter") == 0) {
|
|
return cbm_scope_lookup(ctx->current_scope, "self");
|
|
}
|
|
|
|
/* scoped_identifier: A::B::C — could be a function or a type. */
|
|
if (strcmp(kind, "scoped_identifier") == 0) {
|
|
char *path = rust_node_text(ctx, node);
|
|
if (!path) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *qn = rust_resolve_path_expr(ctx, path);
|
|
if (!qn) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
|
|
if (f && f->signature) {
|
|
return f->signature;
|
|
}
|
|
if (cbm_registry_lookup_type(ctx->registry, qn)) {
|
|
return cbm_type_named(ctx->arena, qn);
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* generic_function: foo::<T> */
|
|
if (strcmp(kind, "generic_function") == 0) {
|
|
TSNode fn = ts_node_child_by_field_name(node, "function", 8);
|
|
if (!ts_node_is_null(fn)) {
|
|
return rust_eval_expr_type(ctx, fn);
|
|
}
|
|
}
|
|
|
|
/* field_expression: obj.field or obj.0 (tuple) */
|
|
if (strcmp(kind, "field_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(node, "value", 5);
|
|
TSNode field = ts_node_child_by_field_name(node, "field", 5);
|
|
if (ts_node_is_null(value) || ts_node_is_null(field)) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const CBMType *recv = rust_eval_expr_type(ctx, value);
|
|
const char *fk = ts_node_type(field);
|
|
if (strcmp(fk, "integer_literal") == 0) {
|
|
/* Tuple field access. */
|
|
if (recv) {
|
|
const CBMType *base = recv;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE) {
|
|
base = base->data.reference.elem;
|
|
}
|
|
if (base && base->kind == CBM_TYPE_TUPLE) {
|
|
char *idx_text = rust_node_text(ctx, field);
|
|
int idx = atoi(idx_text);
|
|
if (idx >= 0 && idx < base->data.tuple.count) {
|
|
return base->data.tuple.elems[idx];
|
|
}
|
|
}
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
char *fname = rust_node_text(ctx, field);
|
|
if (!fname) {
|
|
return cbm_type_unknown();
|
|
}
|
|
return rust_eval_member_access(ctx, recv, fname);
|
|
}
|
|
|
|
/* call_expression: any callable invocation. */
|
|
if (strcmp(kind, "call_expression") == 0) {
|
|
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
|
|
TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9);
|
|
if (ts_node_is_null(func_node)) {
|
|
return cbm_type_unknown();
|
|
}
|
|
const char *fk = ts_node_type(func_node);
|
|
|
|
/* Constructor of a tuple struct, unit-like struct, or stdlib
|
|
* factory invoked via path. Try the registry; on miss, also try
|
|
* UFCS-style method lookup to catch `String::new()`,
|
|
* `Vec::new()`, etc. */
|
|
if (strcmp(fk, "identifier") == 0 || strcmp(fk, "scoped_identifier") == 0) {
|
|
char *path = rust_node_text(ctx, func_node);
|
|
if (path) {
|
|
/* Strip ALL turbofish (`::<...>`) so the lookup below
|
|
* ignores explicit type arguments — handles forms like
|
|
* `Vec::<i32>::new` and `parse::<u32>`. */
|
|
rust_strip_turbofish(path);
|
|
const char *qn = rust_resolve_path_expr(ctx, path);
|
|
if (qn) {
|
|
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, qn);
|
|
if (rt) {
|
|
return cbm_type_named(ctx->arena, qn);
|
|
}
|
|
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
|
|
if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC) {
|
|
const CBMType *const *rt_arr = f->signature->data.func.return_types;
|
|
if (rt_arr && rt_arr[0]) {
|
|
const CBMType *ret = rt_arr[0];
|
|
/* Constructor-style on a stdlib receiver with
|
|
* `unknown` return — substitute the receiver
|
|
* type so chains keep typing. The heuristic
|
|
* matches `new`, `default`, anything starting
|
|
* with `from_` / `with_`, plus a small list of
|
|
* common factory verbs.
|
|
*
|
|
* For smart-pointer factories (`Box::new`,
|
|
* `Rc::new`, `Arc::new`, `RefCell::new`,
|
|
* `Pin::new`, `Mutex::new`, `RwLock::new`)
|
|
* we also try to capture the first call
|
|
* argument's type as a TEMPLATE arg so the
|
|
* Deref chain has something to peel. */
|
|
if (f->receiver_type && cbm_type_is_unknown(ret) && f->short_name &&
|
|
(strcmp(f->short_name, "new") == 0 ||
|
|
strcmp(f->short_name, "default") == 0 ||
|
|
strcmp(f->short_name, "open") == 0 ||
|
|
strcmp(f->short_name, "create") == 0 ||
|
|
strcmp(f->short_name, "create_new") == 0 ||
|
|
strcmp(f->short_name, "bind") == 0 ||
|
|
strcmp(f->short_name, "connect") == 0 ||
|
|
strcmp(f->short_name, "spawn") == 0 ||
|
|
strcmp(f->short_name, "now") == 0 ||
|
|
strncmp(f->short_name, "from_", 5) == 0 ||
|
|
strncmp(f->short_name, "with_", 5) == 0 ||
|
|
strcmp(f->short_name, "from") == 0)) {
|
|
/* Detect smart-pointer wrappers and
|
|
* capture the first arg's type so
|
|
* `let b = Box::new(x); b.method()`
|
|
* dispatches via Deref. */
|
|
bool is_wrapper = strcmp(f->short_name, "new") == 0 &&
|
|
rust_type_derefs_to_first_arg(f->receiver_type);
|
|
if (is_wrapper && !ts_node_is_null(args_node)) {
|
|
uint32_t anc = ts_node_named_child_count(args_node);
|
|
if (anc > 0) {
|
|
const CBMType *arg_t = rust_eval_expr_type(
|
|
ctx, ts_node_named_child(args_node, 0));
|
|
if (arg_t && !cbm_type_is_unknown(arg_t)) {
|
|
return cbm_type_template(ctx->arena, f->receiver_type,
|
|
&arg_t, 1);
|
|
}
|
|
}
|
|
}
|
|
return cbm_type_named(ctx->arena, f->receiver_type);
|
|
}
|
|
/* Self -> receiver_type substitution. */
|
|
if (f->receiver_type && ret->kind == CBM_TYPE_NAMED &&
|
|
strcmp(ret->data.named.qualified_name, "Self") == 0) {
|
|
return cbm_type_named(ctx->arena, f->receiver_type);
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
/* UFCS path lookup: split off short name and try
|
|
* `cbm_registry_lookup_method`. */
|
|
const char *dot = strrchr(qn, '.');
|
|
if (dot) {
|
|
char *head = cbm_arena_strndup(ctx->arena, qn, (size_t)(dot - qn));
|
|
const char *short_name = dot + 1;
|
|
const CBMRegisteredFunc *m =
|
|
cbm_registry_lookup_method_aliased(ctx->registry, head, short_name);
|
|
if (!m && ctx->module_qn) {
|
|
const char *full_head =
|
|
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, head);
|
|
m = cbm_registry_lookup_method_aliased(ctx->registry, full_head,
|
|
short_name);
|
|
if (m)
|
|
head = (char *)full_head;
|
|
}
|
|
if (m && m->signature && m->signature->kind == CBM_TYPE_FUNC &&
|
|
m->signature->data.func.return_types &&
|
|
m->signature->data.func.return_types[0]) {
|
|
const CBMType *ret = m->signature->data.func.return_types[0];
|
|
/* Substitute Self / unknown returns with the
|
|
* receiver type so chained calls keep typing. */
|
|
if (ret->kind == CBM_TYPE_NAMED &&
|
|
strcmp(ret->data.named.qualified_name, "Self") == 0) {
|
|
return cbm_type_named(ctx->arena, head);
|
|
}
|
|
if (cbm_type_is_unknown(ret) &&
|
|
(strcmp(short_name, "new") == 0 ||
|
|
strcmp(short_name, "default") == 0 ||
|
|
strcmp(short_name, "with_capacity") == 0 ||
|
|
strcmp(short_name, "from") == 0)) {
|
|
/* Smart-pointer wrap: capture first arg
|
|
* type into a TEMPLATE so Deref can peel. */
|
|
if (strcmp(short_name, "new") == 0 &&
|
|
rust_type_derefs_to_first_arg(head) &&
|
|
!ts_node_is_null(args_node)) {
|
|
uint32_t anc = ts_node_named_child_count(args_node);
|
|
if (anc > 0) {
|
|
const CBMType *arg_t = rust_eval_expr_type(
|
|
ctx, ts_node_named_child(args_node, 0));
|
|
if (arg_t && !cbm_type_is_unknown(arg_t)) {
|
|
return cbm_type_template(ctx->arena, head, &arg_t, 1);
|
|
}
|
|
}
|
|
}
|
|
return cbm_type_named(ctx->arena, head);
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Method call expressed as field_expression callee. */
|
|
if (strcmp(fk, "field_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(func_node, "value", 5);
|
|
TSNode field = ts_node_child_by_field_name(func_node, "field", 5);
|
|
if (!ts_node_is_null(value) && !ts_node_is_null(field)) {
|
|
const CBMType *recv = rust_eval_expr_type(ctx, value);
|
|
char *mname = rust_node_text(ctx, field);
|
|
if (mname && recv) {
|
|
const CBMType *base = recv;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE) {
|
|
base = base->data.reference.elem;
|
|
}
|
|
/* Template Vec<T> method handling */
|
|
if (base && base->kind == CBM_TYPE_TEMPLATE) {
|
|
const char *tname = base->data.template_type.template_name;
|
|
/* Iterator-producing methods on Vec/&[T]/Option/Result/HashMap. */
|
|
if (strstr(tname, "Vec") || strstr(tname, "VecDeque") ||
|
|
strstr(tname, "HashSet") || strstr(tname, "BTreeSet")) {
|
|
if (strcmp(mname, "iter") == 0 || strcmp(mname, "iter_mut") == 0 ||
|
|
strcmp(mname, "into_iter") == 0 || strcmp(mname, "drain") == 0) {
|
|
/* Iterator<Item=T> — represent loosely as the elem type for our
|
|
* downstream chain calls. Even when the elem type is not
|
|
* known, return Iterator (with no args) so further chain calls
|
|
* can still attribute via Iterator's registered methods. */
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return cbm_type_template(
|
|
ctx->arena, "core.iter.Iterator",
|
|
&base->data.template_type.template_args[0], 1);
|
|
}
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator", NULL, 0);
|
|
}
|
|
/* Methods returning the element type directly. */
|
|
if (strcmp(mname, "remove") == 0 || strcmp(mname, "swap_remove") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return base->data.template_type.template_args[0];
|
|
}
|
|
}
|
|
if (strcmp(mname, "len") == 0 || strcmp(mname, "capacity") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "usize");
|
|
}
|
|
if (strcmp(mname, "is_empty") == 0 || strcmp(mname, "contains") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
if (strcmp(mname, "first") == 0 || strcmp(mname, "last") == 0 ||
|
|
strcmp(mname, "get") == 0 || strcmp(mname, "pop") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
const CBMType *opt_args[1] = {
|
|
base->data.template_type.template_args[0]};
|
|
return cbm_type_template(ctx->arena, "core.option.Option",
|
|
opt_args, 1);
|
|
}
|
|
}
|
|
if (strcmp(mname, "as_slice") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return cbm_type_reference(
|
|
ctx->arena,
|
|
cbm_type_slice(ctx->arena,
|
|
base->data.template_type.template_args[0]));
|
|
}
|
|
}
|
|
}
|
|
if (strstr(tname, "Option")) {
|
|
if (strcmp(mname, "unwrap") == 0 || strcmp(mname, "expect") == 0 ||
|
|
strcmp(mname, "unwrap_or") == 0 ||
|
|
strcmp(mname, "unwrap_or_default") == 0 ||
|
|
strcmp(mname, "unwrap_or_else") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return base->data.template_type.template_args[0];
|
|
}
|
|
}
|
|
if (strcmp(mname, "is_some") == 0 || strcmp(mname, "is_none") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
if (strcmp(mname, "as_ref") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
const CBMType *arg0 = cbm_type_reference(
|
|
ctx->arena, base->data.template_type.template_args[0]);
|
|
return cbm_type_template(ctx->arena, "core.option.Option",
|
|
&arg0, 1);
|
|
}
|
|
}
|
|
}
|
|
if (strstr(tname, "Result")) {
|
|
if (strcmp(mname, "unwrap") == 0 || strcmp(mname, "expect") == 0 ||
|
|
strcmp(mname, "unwrap_or") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return base->data.template_type.template_args[0];
|
|
}
|
|
}
|
|
if (strcmp(mname, "ok") == 0 &&
|
|
base->data.template_type.arg_count > 0) {
|
|
const CBMType *a0 = base->data.template_type.template_args[0];
|
|
return cbm_type_template(ctx->arena, "core.option.Option", &a0, 1);
|
|
}
|
|
if (strcmp(mname, "err") == 0 &&
|
|
base->data.template_type.arg_count > 1) {
|
|
const CBMType *a1 = base->data.template_type.template_args[1];
|
|
return cbm_type_template(ctx->arena, "core.option.Option", &a1, 1);
|
|
}
|
|
if (strcmp(mname, "is_ok") == 0 || strcmp(mname, "is_err") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
}
|
|
if (strstr(tname, "Iterator")) {
|
|
/* map/filter/take/skip/rev/chain → Iterator (with the relevant elem) */
|
|
if (strcmp(mname, "collect") == 0) {
|
|
/* Often Vec<Item>; without turbofish info we model as Vec<elem>. */
|
|
if (base->data.template_type.arg_count > 0) {
|
|
return cbm_type_template(ctx->arena, "alloc.vec.Vec",
|
|
base->data.template_type.template_args,
|
|
base->data.template_type.arg_count);
|
|
}
|
|
}
|
|
if (strcmp(mname, "count") == 0 || strcmp(mname, "len") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "usize");
|
|
}
|
|
if (strcmp(mname, "next") == 0 || strcmp(mname, "last") == 0 ||
|
|
strcmp(mname, "nth") == 0 || strcmp(mname, "find") == 0 ||
|
|
strcmp(mname, "max") == 0 || strcmp(mname, "min") == 0 ||
|
|
strcmp(mname, "max_by") == 0 || strcmp(mname, "min_by") == 0) {
|
|
if (base->data.template_type.arg_count > 0) {
|
|
const CBMType *a0 = base->data.template_type.template_args[0];
|
|
return cbm_type_template(ctx->arena, "core.option.Option", &a0,
|
|
1);
|
|
}
|
|
}
|
|
if (strcmp(mname, "filter") == 0 || strcmp(mname, "take") == 0 ||
|
|
strcmp(mname, "skip") == 0 || strcmp(mname, "rev") == 0 ||
|
|
strcmp(mname, "cloned") == 0 || strcmp(mname, "copied") == 0 ||
|
|
strcmp(mname, "step_by") == 0 || strcmp(mname, "fuse") == 0 ||
|
|
strcmp(mname, "peekable") == 0 || strcmp(mname, "by_ref") == 0 ||
|
|
strcmp(mname, "take_while") == 0 ||
|
|
strcmp(mname, "skip_while") == 0 || strcmp(mname, "inspect") == 0) {
|
|
return base; /* preserves Iterator<Item> */
|
|
}
|
|
if ((strcmp(mname, "sum") == 0 || strcmp(mname, "product") == 0 ||
|
|
strcmp(mname, "fold") == 0 || strcmp(mname, "reduce") == 0) &&
|
|
base->data.template_type.arg_count > 0) {
|
|
return base->data.template_type.template_args[0];
|
|
}
|
|
if (strcmp(mname, "any") == 0 || strcmp(mname, "all") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
if (strcmp(mname, "position") == 0) {
|
|
const CBMType *usize = cbm_type_builtin(ctx->arena, "usize");
|
|
return cbm_type_template(ctx->arena, "core.option.Option", &usize,
|
|
1);
|
|
}
|
|
if (strcmp(mname, "enumerate") == 0 &&
|
|
base->data.template_type.arg_count > 0) {
|
|
/* Iterator<(usize, T)>. */
|
|
const CBMType *pair[2] = {
|
|
cbm_type_builtin(ctx->arena, "usize"),
|
|
base->data.template_type.template_args[0]};
|
|
const CBMType *tup = cbm_type_tuple(ctx->arena, pair, 2);
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator", &tup, 1);
|
|
}
|
|
}
|
|
/* HashMap<K, V> / BTreeMap<K, V> generics. */
|
|
if (strstr(tname, "HashMap") || strstr(tname, "BTreeMap")) {
|
|
if (strcmp(mname, "len") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "usize");
|
|
}
|
|
if (strcmp(mname, "is_empty") == 0 ||
|
|
strcmp(mname, "contains_key") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
if (strcmp(mname, "get") == 0 || strcmp(mname, "get_mut") == 0 ||
|
|
strcmp(mname, "remove") == 0) {
|
|
if (base->data.template_type.arg_count > 1) {
|
|
const CBMType *v = base->data.template_type.template_args[1];
|
|
return cbm_type_template(ctx->arena, "core.option.Option", &v,
|
|
1);
|
|
}
|
|
}
|
|
if (strcmp(mname, "iter") == 0 || strcmp(mname, "iter_mut") == 0) {
|
|
/* Iterator<(K, V)>. */
|
|
if (base->data.template_type.arg_count > 1) {
|
|
const CBMType *pair[2] = {
|
|
base->data.template_type.template_args[0],
|
|
base->data.template_type.template_args[1]};
|
|
const CBMType *tup = cbm_type_tuple(ctx->arena, pair, 2);
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator", &tup,
|
|
1);
|
|
}
|
|
}
|
|
if (strcmp(mname, "keys") == 0 &&
|
|
base->data.template_type.arg_count > 0) {
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator",
|
|
&base->data.template_type.template_args[0],
|
|
1);
|
|
}
|
|
if (strcmp(mname, "values") == 0 &&
|
|
base->data.template_type.arg_count > 1) {
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator",
|
|
&base->data.template_type.template_args[1],
|
|
1);
|
|
}
|
|
}
|
|
}
|
|
/* Fall through: registered method on the named type.
|
|
* Unwrap the resulting FUNC signature to its first
|
|
* return type so chains like
|
|
* `String::new().to_uppercase().len()` keep typing
|
|
* across each link. */
|
|
const CBMType *t = rust_eval_member_access(ctx, recv, mname);
|
|
if (t && t->kind == CBM_TYPE_FUNC && t->data.func.return_types &&
|
|
t->data.func.return_types[0]) {
|
|
const CBMType *ret = t->data.func.return_types[0];
|
|
/* Self -> receiver. */
|
|
if (ret->kind == CBM_TYPE_NAMED &&
|
|
strcmp(ret->data.named.qualified_name, "Self") == 0) {
|
|
const CBMType *rb = recv;
|
|
while (rb && rb->kind == CBM_TYPE_REFERENCE)
|
|
rb = rb->data.reference.elem;
|
|
if (rb && rb->kind == CBM_TYPE_NAMED) {
|
|
return cbm_type_named(ctx->arena, rb->data.named.qualified_name);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
return t;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Fallback: function expression's return type. */
|
|
const CBMType *func_type = rust_eval_expr_type(ctx, func_node);
|
|
if (func_type && func_type->kind == CBM_TYPE_FUNC && func_type->data.func.return_types &&
|
|
func_type->data.func.return_types[0]) {
|
|
return func_type->data.func.return_types[0];
|
|
}
|
|
if (func_type && func_type->kind == CBM_TYPE_NAMED) {
|
|
return func_type;
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* macro_invocation: vec!, format!, … */
|
|
if (strcmp(kind, "macro_invocation") == 0) {
|
|
TSNode mname = ts_node_child_by_field_name(node, "macro", 5);
|
|
if (ts_node_is_null(mname) && ts_node_named_child_count(node) > 0) {
|
|
mname = ts_node_named_child(node, 0);
|
|
}
|
|
char *name = rust_node_text(ctx, mname);
|
|
if (!name) {
|
|
return cbm_type_unknown();
|
|
}
|
|
if (strcmp(name, "vec") == 0) {
|
|
/* vec![T] — peek first argument's type. */
|
|
TSNode args = ts_node_child_by_field_name(node, "arguments", 9);
|
|
if (!ts_node_is_null(args)) {
|
|
/* args is a token_tree; skim its named children for the first
|
|
* expression token. */
|
|
uint32_t nc = ts_node_named_child_count(args);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_named_child(args, i);
|
|
const CBMType *elem = rust_eval_expr_type(ctx, c);
|
|
if (elem && !cbm_type_is_unknown(elem)) {
|
|
return cbm_type_template(ctx->arena, "alloc.vec.Vec", &elem, 1);
|
|
}
|
|
}
|
|
}
|
|
return cbm_type_template(ctx->arena, "alloc.vec.Vec", NULL, 0);
|
|
}
|
|
if (is_string_macro(name)) {
|
|
return cbm_type_named(ctx->arena, "alloc.string.String");
|
|
}
|
|
if (is_void_macro(name)) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* reference_expression: &x or &mut x */
|
|
if (strcmp(kind, "reference_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(node, "value", 5);
|
|
if (ts_node_is_null(value) && ts_node_named_child_count(node) > 0) {
|
|
value = ts_node_named_child(node, 0);
|
|
}
|
|
return cbm_type_reference(ctx->arena, rust_eval_expr_type(ctx, value));
|
|
}
|
|
|
|
/* unary_expression — *x dereferences, !x is bool, -x same as operand */
|
|
if (strcmp(kind, "unary_expression") == 0) {
|
|
TSNode operand = ts_node_named_child_count(node) > 0
|
|
? ts_node_named_child(node, ts_node_named_child_count(node) - 1)
|
|
: (TSNode){0};
|
|
char *op = NULL;
|
|
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (!ts_node_is_named(c)) {
|
|
op = rust_node_text(ctx, c);
|
|
if (op)
|
|
break;
|
|
}
|
|
}
|
|
const CBMType *inner =
|
|
ts_node_is_null(operand) ? cbm_type_unknown() : rust_eval_expr_type(ctx, operand);
|
|
if (op && strcmp(op, "*") == 0) {
|
|
if (inner && inner->kind == CBM_TYPE_REFERENCE) {
|
|
return inner->data.reference.elem;
|
|
}
|
|
if (inner && inner->kind == CBM_TYPE_POINTER) {
|
|
return inner->data.pointer.elem;
|
|
}
|
|
return inner;
|
|
}
|
|
if (op && strcmp(op, "!") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
return inner;
|
|
}
|
|
|
|
/* binary_expression — comparisons → bool, logical → bool, arith → left */
|
|
if (strcmp(kind, "binary_expression") == 0) {
|
|
TSNode left = ts_node_child_by_field_name(node, "left", 4);
|
|
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (ts_node_is_named(c))
|
|
continue;
|
|
char *op = rust_node_text(ctx, c);
|
|
if (!op)
|
|
continue;
|
|
if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || strcmp(op, "<") == 0 ||
|
|
strcmp(op, ">") == 0 || strcmp(op, "<=") == 0 || strcmp(op, ">=") == 0 ||
|
|
strcmp(op, "&&") == 0 || strcmp(op, "||") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "bool");
|
|
}
|
|
break;
|
|
}
|
|
if (!ts_node_is_null(left)) {
|
|
return rust_eval_expr_type(ctx, left);
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* index_expression */
|
|
if (strcmp(kind, "index_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(node, "value", 5);
|
|
if (ts_node_is_null(value) && ts_node_named_child_count(node) > 0) {
|
|
value = ts_node_named_child(node, 0);
|
|
}
|
|
const CBMType *op_type = rust_eval_expr_type(ctx, value);
|
|
const CBMType *base = op_type;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE)
|
|
base = base->data.reference.elem;
|
|
if (base && base->kind == CBM_TYPE_SLICE) {
|
|
return base->data.slice.elem;
|
|
}
|
|
if (base && base->kind == CBM_TYPE_TEMPLATE) {
|
|
const char *nm = base->data.template_type.template_name;
|
|
if ((strstr(nm, "Vec") || strstr(nm, "VecDeque")) &&
|
|
base->data.template_type.arg_count > 0) {
|
|
return base->data.template_type.template_args[0];
|
|
}
|
|
if ((strstr(nm, "HashMap") || strstr(nm, "BTreeMap")) &&
|
|
base->data.template_type.arg_count > 1) {
|
|
return base->data.template_type.template_args[1];
|
|
}
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* parenthesized_expression */
|
|
if (strcmp(kind, "parenthesized_expression") == 0 && ts_node_named_child_count(node) > 0) {
|
|
return rust_eval_expr_type(ctx, ts_node_named_child(node, 0));
|
|
}
|
|
|
|
/* try_expression: e? — peel one Result<T, _> / Option<T> layer. */
|
|
if (strcmp(kind, "try_expression") == 0 && ts_node_named_child_count(node) > 0) {
|
|
const CBMType *inner = rust_eval_expr_type(ctx, ts_node_named_child(node, 0));
|
|
if (inner && inner->kind == CBM_TYPE_TEMPLATE) {
|
|
const char *nm = inner->data.template_type.template_name;
|
|
if ((strstr(nm, "Result") || strstr(nm, "Option")) &&
|
|
inner->data.template_type.arg_count > 0) {
|
|
return inner->data.template_type.template_args[0];
|
|
}
|
|
}
|
|
return inner;
|
|
}
|
|
|
|
/* await_expression: future.await — peel one Future / Poll layer naively. */
|
|
if (strcmp(kind, "await_expression") == 0 && ts_node_named_child_count(node) > 0) {
|
|
const CBMType *inner = rust_eval_expr_type(ctx, ts_node_named_child(node, 0));
|
|
if (inner && inner->kind == CBM_TYPE_TEMPLATE && inner->data.template_type.arg_count > 0) {
|
|
return inner->data.template_type.template_args[0];
|
|
}
|
|
return inner;
|
|
}
|
|
|
|
/* type_cast_expression: x as T */
|
|
if (strcmp(kind, "type_cast_expression") == 0) {
|
|
TSNode tn = ts_node_child_by_field_name(node, "type", 4);
|
|
if (!ts_node_is_null(tn)) {
|
|
return rust_parse_type_node(ctx, tn);
|
|
}
|
|
}
|
|
|
|
/* tuple_expression */
|
|
if (strcmp(kind, "tuple_expression") == 0) {
|
|
const CBMType *elems[16];
|
|
int count = 0;
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < nc && count < 16; i++) {
|
|
elems[count++] = rust_eval_expr_type(ctx, ts_node_named_child(node, i));
|
|
}
|
|
if (count == 0) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
if (count == 1) {
|
|
return elems[0];
|
|
}
|
|
return cbm_type_tuple(ctx->arena, elems, count);
|
|
}
|
|
|
|
/* array_expression: [a, b, c] */
|
|
if (strcmp(kind, "array_expression") == 0) {
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
const CBMType *elem = rust_eval_expr_type(ctx, ts_node_named_child(node, 0));
|
|
return cbm_type_slice(ctx->arena, elem);
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* range_expression: a..b, a..=b, a.., ..b, .. — model as
|
|
* Iterator<elem> so chains like `(0..n).map(...).count()` keep
|
|
* typing. The element type is the type of the start/end expr. */
|
|
if (strcmp(kind, "range_expression") == 0) {
|
|
const CBMType *elem_type = NULL;
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_named_child(node, i);
|
|
const CBMType *t = rust_eval_expr_type(ctx, c);
|
|
if (t && !cbm_type_is_unknown(t)) {
|
|
elem_type = t;
|
|
break;
|
|
}
|
|
}
|
|
if (elem_type) {
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator", &elem_type, 1);
|
|
}
|
|
return cbm_type_template(ctx->arena, "core.iter.Iterator", NULL, 0);
|
|
}
|
|
|
|
/* unit_expression */
|
|
if (strcmp(kind, "unit_expression") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
|
|
/* struct_expression: Foo { … } */
|
|
if (strcmp(kind, "struct_expression") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
|
|
if (ts_node_is_null(name_node) && ts_node_named_child_count(node) > 0) {
|
|
name_node = ts_node_named_child(node, 0);
|
|
}
|
|
if (!ts_node_is_null(name_node)) {
|
|
char *path = rust_node_text(ctx, name_node);
|
|
if (path) {
|
|
return cbm_type_named(ctx->arena, rust_resolve_path_expr(ctx, path));
|
|
}
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* if_expression / match_expression / block / loop_expression — value of
|
|
* the trailing expression in the consequence. */
|
|
if (strcmp(kind, "if_expression") == 0) {
|
|
TSNode cons = ts_node_child_by_field_name(node, "consequence", 11);
|
|
if (!ts_node_is_null(cons)) {
|
|
return rust_eval_expr_type(ctx, cons);
|
|
}
|
|
}
|
|
if (strcmp(kind, "match_expression") == 0) {
|
|
/* Take the type of the first arm's value if available. */
|
|
TSNode body = ts_node_child_by_field_name(node, "body", 4);
|
|
if (!ts_node_is_null(body)) {
|
|
uint32_t nc = ts_node_named_child_count(body);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode arm = ts_node_named_child(body, i);
|
|
if (strcmp(ts_node_type(arm), "match_arm") == 0) {
|
|
TSNode v = ts_node_child_by_field_name(arm, "value", 5);
|
|
if (!ts_node_is_null(v)) {
|
|
return rust_eval_expr_type(ctx, v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (strcmp(kind, "block") == 0) {
|
|
/* Find the last expression child (the trailing expression of the block). */
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
TSNode last = {0};
|
|
bool found = false;
|
|
for (uint32_t i = nc; i > 0; i--) {
|
|
TSNode c = ts_node_named_child(node, i - 1);
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "expression_statement") != 0 && strcmp(ck, "let_declaration") != 0 &&
|
|
strcmp(ck, "empty_statement") != 0 && strcmp(ck, "line_comment") != 0 &&
|
|
strcmp(ck, "block_comment") != 0) {
|
|
last = c;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
return rust_eval_expr_type(ctx, last);
|
|
}
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
if (strcmp(kind, "loop_expression") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "()");
|
|
}
|
|
|
|
/* closure_expression: |a, b| body — best effort. Return type comes from
|
|
* the body's last expression if present. */
|
|
if (strcmp(kind, "closure_expression") == 0) {
|
|
TSNode body = ts_node_child_by_field_name(node, "body", 4);
|
|
if (ts_node_is_null(body)) {
|
|
return cbm_type_func(ctx->arena, NULL, NULL, NULL);
|
|
}
|
|
return cbm_type_func(ctx->arena, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* Literals fall through. */
|
|
if (strcmp(kind, "integer_literal") == 0 || strcmp(kind, "float_literal") == 0 ||
|
|
strcmp(kind, "string_literal") == 0 || strcmp(kind, "raw_string_literal") == 0 ||
|
|
strcmp(kind, "char_literal") == 0 || strcmp(kind, "boolean_literal") == 0 ||
|
|
strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0) {
|
|
return rust_eval_literal_type(ctx, kind, node);
|
|
}
|
|
|
|
/* break / continue / return — expressions with no useful type for callers. */
|
|
if (strcmp(kind, "return_expression") == 0 || strcmp(kind, "break_expression") == 0 ||
|
|
strcmp(kind, "continue_expression") == 0 || strcmp(kind, "yield_expression") == 0) {
|
|
return cbm_type_builtin(ctx->arena, "!");
|
|
}
|
|
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* Bidirectional wrapper. Evaluate `node` with an `expected` hint. The
|
|
* hint is used post-synthesis: if synthesis returned an under-specified
|
|
* type (unknown, or a TEMPLATE without args, or a NAMED that the hint
|
|
* refines), we substitute the hint when it matches structurally. */
|
|
const CBMType *rust_eval_expr_typed(RustLSPContext *ctx, TSNode node, const CBMType *expected) {
|
|
const CBMType *synth = rust_eval_expr_type(ctx, node);
|
|
if (!expected)
|
|
return synth;
|
|
if (!synth || cbm_type_is_unknown(synth)) {
|
|
return expected;
|
|
}
|
|
/* Template with same head and missing args -> use expected. */
|
|
if (synth->kind == CBM_TYPE_TEMPLATE && expected->kind == CBM_TYPE_TEMPLATE &&
|
|
synth->data.template_type.template_name && expected->data.template_type.template_name &&
|
|
strcmp(synth->data.template_type.template_name,
|
|
expected->data.template_type.template_name) == 0 &&
|
|
synth->data.template_type.arg_count == 0 && expected->data.template_type.arg_count > 0) {
|
|
return expected;
|
|
}
|
|
/* NAMED matches expected NAMED — no refinement needed. */
|
|
if (synth->kind == CBM_TYPE_NAMED && expected->kind == CBM_TYPE_TEMPLATE &&
|
|
synth->data.named.qualified_name && expected->data.template_type.template_name &&
|
|
strcmp(synth->data.named.qualified_name, expected->data.template_type.template_name) == 0) {
|
|
return expected; /* refine NAMED → TEMPLATE with args */
|
|
}
|
|
return synth;
|
|
}
|
|
|
|
/* Member access type: returns the CBMType for `recv.field_or_method`.
|
|
* Methods return their FUNC signature; fields return the field type. */
|
|
static const CBMType *rust_eval_member_access(RustLSPContext *ctx, const CBMType *recv,
|
|
const char *member) {
|
|
if (!recv || !member)
|
|
return cbm_type_unknown();
|
|
/* Auto-deref through references. */
|
|
const CBMType *base = recv;
|
|
while (base && (base->kind == CBM_TYPE_REFERENCE || base->kind == CBM_TYPE_POINTER)) {
|
|
base = (base->kind == CBM_TYPE_REFERENCE) ? base->data.reference.elem
|
|
: base->data.pointer.elem;
|
|
}
|
|
if (!base)
|
|
return cbm_type_unknown();
|
|
|
|
const char *type_qn = NULL;
|
|
const CBMType **template_args = NULL;
|
|
int template_count = 0;
|
|
const char **template_params = NULL;
|
|
|
|
if (base->kind == CBM_TYPE_NAMED) {
|
|
type_qn = base->data.named.qualified_name;
|
|
} else if (base->kind == CBM_TYPE_TEMPLATE) {
|
|
type_qn = base->data.template_type.template_name;
|
|
template_args = (const CBMType **)base->data.template_type.template_args;
|
|
template_count = base->data.template_type.arg_count;
|
|
} else if (base->kind == CBM_TYPE_BUILTIN) {
|
|
/* Map a few primitives into stdlib QNs so registered methods on
|
|
* `str`/`String`/integers are findable. */
|
|
const char *nm = base->data.builtin.name;
|
|
if (strcmp(nm, "str") == 0)
|
|
type_qn = "core.str";
|
|
else if (strcmp(nm, "String") == 0)
|
|
type_qn = "alloc.string.String";
|
|
else
|
|
type_qn = nm;
|
|
} else if (base->kind == CBM_TYPE_SLICE) {
|
|
type_qn = "core.slice";
|
|
} else {
|
|
return cbm_type_unknown();
|
|
}
|
|
if (!type_qn)
|
|
return cbm_type_unknown();
|
|
|
|
/* Check inherent method first. */
|
|
const CBMRegisteredFunc *method = rust_lookup_method(ctx, type_qn, member);
|
|
if (method && method->signature) {
|
|
if (template_args && method->type_param_names) {
|
|
template_params = method->type_param_names;
|
|
const CBMType *sub =
|
|
rust_substitute_type(ctx->arena, method->signature, template_params, template_args);
|
|
return sub;
|
|
}
|
|
/* Substitute any Self placeholder on the return type. */
|
|
const CBMType *sig = method->signature;
|
|
if (sig && sig->kind == CBM_TYPE_FUNC && sig->data.func.return_types &&
|
|
sig->data.func.return_types[0]) {
|
|
const CBMType *ret = sig->data.func.return_types[0];
|
|
if (ret && ret->kind == CBM_TYPE_NAMED &&
|
|
strcmp(ret->data.named.qualified_name, "Self") == 0) {
|
|
return cbm_type_named(ctx->arena, type_qn);
|
|
}
|
|
}
|
|
return method->signature;
|
|
}
|
|
|
|
/* Field on the type's RegisteredType. */
|
|
const CBMType *ft = rust_lookup_field(ctx, type_qn, member, 0);
|
|
if (ft) {
|
|
if (template_args && template_params) {
|
|
return rust_substitute_type(ctx->arena, ft, template_params, template_args);
|
|
}
|
|
return ft;
|
|
}
|
|
return cbm_type_unknown();
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 7. Method dispatch + field lookup
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
static const CBMType *rust_lookup_field(RustLSPContext *ctx, const char *type_qn,
|
|
const char *field_name, int depth) {
|
|
if (!type_qn || !field_name || depth > CBM_LSP_MAX_LOOKUP_DEPTH)
|
|
return NULL;
|
|
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
|
|
if (!rt)
|
|
return NULL;
|
|
if (rt->alias_of)
|
|
return rust_lookup_field(ctx, rt->alias_of, field_name, depth + 1);
|
|
if (rt->field_names) {
|
|
for (int i = 0; rt->field_names[i]; i++) {
|
|
if (strcmp(rt->field_names[i], field_name) == 0 && rt->field_types &&
|
|
rt->field_types[i]) {
|
|
return rt->field_types[i];
|
|
}
|
|
}
|
|
}
|
|
if (rt->embedded_types) {
|
|
for (int i = 0; rt->embedded_types[i]; i++) {
|
|
const CBMType *f = rust_lookup_field(ctx, rt->embedded_types[i], field_name, depth + 1);
|
|
if (f)
|
|
return f;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Hardcoded Deref<Target=U> map for stdlib smart pointers + guards. The
|
|
* format mirrors the de-facto rule: `<smart-pointer-QN-prefix>` →
|
|
* `<inner-type position>` where inner is taken from the receiver's
|
|
* template args. We don't store U literally; instead we let the call
|
|
* site pass `template_args[idx]` as the new receiver type for retry. */
|
|
static bool rust_type_derefs_to_first_arg(const char *type_qn) {
|
|
if (!type_qn)
|
|
return false;
|
|
static const char *derefable[] = {"alloc.boxed.Box",
|
|
"std.boxed.Box",
|
|
"alloc.rc.Rc",
|
|
"std.rc.Rc",
|
|
"alloc.sync.Arc",
|
|
"std.sync.Arc",
|
|
"core.cell.RefCell",
|
|
"std.cell.RefCell",
|
|
"core.cell.Cell",
|
|
"std.cell.Cell",
|
|
"std.sync.MutexGuard",
|
|
"std.sync.RwLockReadGuard",
|
|
"std.sync.RwLockWriteGuard",
|
|
"core.pin.Pin",
|
|
NULL};
|
|
for (const char **p = derefable; *p; p++) {
|
|
if (strcmp(*p, type_qn) == 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Returns the inner type after one Deref step. For NAMED types we check
|
|
* for a registered `embedded_types` entry whose tail is "Target=<U>" or,
|
|
* for stdlib smart pointers, peel the first template arg. Returns NULL
|
|
* if no Deref relationship is known. */
|
|
static const CBMType *rust_deref_step(RustLSPContext *ctx, const CBMType *t) {
|
|
if (!t)
|
|
return NULL;
|
|
/* Smart pointers: Box<T>, Rc<T>, Arc<T>, etc. The grammar gives us a
|
|
* TEMPLATE; the inner type is the first template arg. */
|
|
if (t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_name) {
|
|
if (rust_type_derefs_to_first_arg(t->data.template_type.template_name)) {
|
|
if (t->data.template_type.arg_count > 0) {
|
|
return t->data.template_type.template_args[0];
|
|
}
|
|
return NULL;
|
|
}
|
|
}
|
|
/* NAMED smart-pointer (rare — only when the user wrote `Rc` without
|
|
* type arg) — no inner to peel. */
|
|
if (t->kind == CBM_TYPE_NAMED && t->data.named.qualified_name) {
|
|
if (rust_type_derefs_to_first_arg(t->data.named.qualified_name)) {
|
|
return NULL;
|
|
}
|
|
/* Project type with a registered Deref impl. We approximate this
|
|
* by checking `embedded_types` for an entry of the form
|
|
* `<TraitQN>:DerefTarget:<U>` — set up by `impl Deref for X`
|
|
* post-processing. (Currently not produced by extract_defs; the
|
|
* call still works for stdlib types.) */
|
|
const CBMRegisteredType *rt =
|
|
cbm_registry_lookup_type(ctx->registry, t->data.named.qualified_name);
|
|
if (rt && rt->embedded_types) {
|
|
for (int i = 0; rt->embedded_types[i]; i++) {
|
|
const char *e = rt->embedded_types[i];
|
|
static const char prefix[] = "DerefTarget:";
|
|
if (strncmp(e, prefix, sizeof(prefix) - 1) == 0) {
|
|
return cbm_type_named(ctx->arena, e + sizeof(prefix) - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static const CBMRegisteredFunc *rust_lookup_method_depth(RustLSPContext *ctx, const char *type_qn,
|
|
const char *member_name, int depth) {
|
|
if (!type_qn || !member_name)
|
|
return NULL;
|
|
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH)
|
|
return NULL;
|
|
|
|
/* Direct inherent method lookup. */
|
|
const CBMRegisteredFunc *f =
|
|
cbm_registry_lookup_method_aliased(ctx->registry, type_qn, member_name);
|
|
if (f)
|
|
return f;
|
|
|
|
/* Follow alias / embedded chain. */
|
|
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
|
|
if (rt) {
|
|
if (rt->alias_of) {
|
|
f = rust_lookup_method_depth(ctx, rt->alias_of, member_name, depth + 1);
|
|
if (f)
|
|
return f;
|
|
}
|
|
if (rt->embedded_types) {
|
|
for (int i = 0; rt->embedded_types[i]; i++) {
|
|
/* Skip the synthetic DerefTarget marker — handled by the
|
|
* caller in the receiver-walk loop. */
|
|
if (strncmp(rt->embedded_types[i], "DerefTarget:", 12) == 0)
|
|
continue;
|
|
/* Skip bound markers used for type-param trait bound
|
|
* recording — those live under "Bound:<TraitQN>". */
|
|
if (strncmp(rt->embedded_types[i], "Bound:", 6) == 0) {
|
|
/* Dispatch through the bound trait. */
|
|
f = rust_lookup_method_depth(ctx, rt->embedded_types[i] + 6, member_name,
|
|
depth + 1);
|
|
if (f)
|
|
return f;
|
|
continue;
|
|
}
|
|
f = rust_lookup_method_depth(ctx, rt->embedded_types[i], member_name, depth + 1);
|
|
if (f)
|
|
return f;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *type_qn,
|
|
const char *member_name) {
|
|
return rust_lookup_method_depth(ctx, type_qn, member_name, 0);
|
|
}
|
|
|
|
/* Walk the registry for any method named `member_name` on a type that
|
|
* implements `trait_qn`. We approximate trait impl membership by checking
|
|
* whether the type's `embedded_types` contains `trait_qn` (we treat
|
|
* `impl Trait for Type` as registering the trait QN as an embedded type
|
|
* of the receiver). */
|
|
static const CBMRegisteredFunc *rust_lookup_method_in_trait(RustLSPContext *ctx,
|
|
const char *trait_qn,
|
|
const char *method_name) {
|
|
if (!ctx || !trait_qn || !method_name)
|
|
return NULL;
|
|
const CBMTypeRegistry *reg = ctx->registry;
|
|
/* Trait method is also registered on the trait itself with `receiver_type`
|
|
* set to the trait QN (default impls / signatures). */
|
|
return cbm_registry_lookup_method(reg, trait_qn, method_name);
|
|
}
|
|
|
|
/* For a method call where the receiver is a `dyn Trait` or `impl Trait`,
|
|
* try to resolve through the trait's known impls. Returns the chosen
|
|
* concrete method, the trait method (default impl), or NULL. */
|
|
static const CBMRegisteredFunc *rust_resolve_trait_method(RustLSPContext *ctx,
|
|
const char *receiver_type_qn,
|
|
const char *method_name,
|
|
int *out_impl_count) {
|
|
if (out_impl_count)
|
|
*out_impl_count = 0;
|
|
if (!ctx || !receiver_type_qn || !method_name)
|
|
return NULL;
|
|
const CBMTypeRegistry *reg = ctx->registry;
|
|
|
|
/* First try inherent method on the receiver itself, following any
|
|
* type aliases (so `std.sync.Arc.clone` resolves through to
|
|
* `alloc.sync.Arc.clone` in the registry). Stays BEFORE the memo check —
|
|
* a colliding real hit is always found (collision guard). */
|
|
const CBMRegisteredFunc *inh =
|
|
cbm_registry_lookup_method_aliased(reg, receiver_type_qn, method_name);
|
|
if (inh) {
|
|
if (out_impl_count)
|
|
*out_impl_count = 1;
|
|
return inh;
|
|
}
|
|
|
|
/* Negative memo (sealed registry only): this whole cascade — embedded-
|
|
* impl walk + trait-default tail — reads nothing but the registry and the
|
|
* two query strings, so under read_only a miss is a pure fact. Macro-
|
|
* expanded kernel rust asks the same failing (receiver, method) question
|
|
* thousands of times per file; without the memo each repeat re-paid the
|
|
* full walk (~63 s per trait-heavy file). Only the full miss (0 impls,
|
|
* no trait default) is memoized, preserving out_impl_count fidelity for
|
|
* the ambiguous (>=2 impls) case. */
|
|
bool nm_active = reg && reg->read_only;
|
|
uint64_t nm_key = 0;
|
|
if (nm_active) {
|
|
nm_key = cbm_negmemo_key(1, receiver_type_qn, method_name);
|
|
if (cbm_negmemo_contains(&ctx->neg_memo, nm_key)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/* Look at every type whose embedded_types include the receiver_type_qn
|
|
* (treated as a trait): pick the single-impl case. Prefilter to the types whose
|
|
* embedded_types carry a matching BARE name via the registry index; the exact
|
|
* full-QN check below is unchanged, so the result set is identical. */
|
|
const CBMRegisteredFunc *unique = NULL;
|
|
int impls = 0;
|
|
const char *rdot = strrchr(receiver_type_qn, '.');
|
|
const char *rbare = rdot ? rdot + 1 : receiver_type_qn;
|
|
CBMTypeEmbedIter eit;
|
|
cbm_registry_types_by_embedded_bare(reg, rbare, &eit);
|
|
for (int ti; impls < 3 && (ti = cbm_type_embed_iter_next(&eit)) >= 0;) {
|
|
const CBMRegisteredType *t = ®->types[ti];
|
|
if (!t->embedded_types)
|
|
continue;
|
|
for (int j = 0; t->embedded_types[j]; j++) {
|
|
if (strcmp(t->embedded_types[j], receiver_type_qn) == 0) {
|
|
const CBMRegisteredFunc *mf =
|
|
cbm_registry_lookup_method(reg, t->qualified_name, method_name);
|
|
if (mf) {
|
|
impls++;
|
|
if (impls == 1)
|
|
unique = mf;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (out_impl_count)
|
|
*out_impl_count = impls;
|
|
if (impls == 1)
|
|
return unique;
|
|
const CBMRegisteredFunc *tm =
|
|
rust_lookup_method_in_trait(ctx, receiver_type_qn, method_name);
|
|
if (nm_active && !tm && impls == 0) {
|
|
cbm_negmemo_insert(&ctx->neg_memo, ctx->arena, nm_key);
|
|
}
|
|
return tm;
|
|
}
|
|
|
|
// True if `type_qn` implements a trait that declares `method_name` — i.e. a
|
|
// method resolved inherently on the receiver is actually a trait-impl method
|
|
// (lsp_trait_dispatch) rather than a plain inherent one (lsp_method_dispatch).
|
|
// A struct's embedded_types are the traits it implements (the impl-link model
|
|
// rust_resolve_trait_method already relies on), so a declaring trait among them
|
|
// means the method came from `impl Trait for Type`.
|
|
static bool rust_method_is_trait_impl(RustLSPContext *ctx, const char *type_qn,
|
|
const char *method_name) {
|
|
if (!ctx || !type_qn || !method_name)
|
|
return false;
|
|
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
|
|
if (!rt || !rt->embedded_types)
|
|
return false;
|
|
for (int i = 0; rt->embedded_types[i]; i++) {
|
|
if (cbm_registry_lookup_method(ctx->registry, rt->embedded_types[i], method_name))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Find the sole concrete implementer of trait `trait_qn` that declares
|
|
// `method_name`, returning that impl's method (NULL if none or 2+), setting
|
|
// *out_n to the count (capped at 2). Used for `Trait::method` UFCS so it
|
|
// resolves to the concrete impl rather than the trait's own abstract method.
|
|
// Matches the embedded (impl-link) entry by full QN OR bare name, since the
|
|
// link is recorded short in some registry entries and fully-qualified in
|
|
// others; dedups implementers by QN.
|
|
static const CBMRegisteredFunc *rust_find_sole_trait_impl(RustLSPContext *ctx, const char *trait_qn,
|
|
const char *method_name, int *out_n) {
|
|
if (out_n)
|
|
*out_n = 0;
|
|
if (!ctx || !trait_qn || !method_name)
|
|
return NULL;
|
|
const CBMTypeRegistry *reg = ctx->registry;
|
|
/* Negative memo (sealed registry only) — registry-pure cascade; only the
|
|
* zero-implementer miss is memoized (out_n fidelity for the 2+ case). */
|
|
bool nm_active = reg && reg->read_only;
|
|
uint64_t nm_key = 0;
|
|
if (nm_active) {
|
|
nm_key = cbm_negmemo_key(2, trait_qn, method_name);
|
|
if (cbm_negmemo_contains(&ctx->neg_memo, nm_key)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
const char *tdot = strrchr(trait_qn, '.');
|
|
const char *tbare = tdot ? tdot + 1 : trait_qn;
|
|
const CBMRegisteredFunc *first = NULL;
|
|
const char *first_qn = NULL;
|
|
int n = 0;
|
|
/* Prefilter to types whose embedded_types carry the trait's BARE name; the
|
|
* exact (full-QN OR bare) check below is unchanged. tbare-keyed index captures
|
|
* every original match (a full-QN match implies a bare-name match). */
|
|
CBMTypeEmbedIter eit;
|
|
cbm_registry_types_by_embedded_bare(reg, tbare, &eit);
|
|
for (int ti; n < 2 && (ti = cbm_type_embed_iter_next(&eit)) >= 0;) {
|
|
const CBMRegisteredType *t = ®->types[ti];
|
|
if (!t->embedded_types || !t->qualified_name)
|
|
continue;
|
|
bool impls = false;
|
|
for (int j = 0; t->embedded_types[j]; j++) {
|
|
const char *e = t->embedded_types[j];
|
|
const char *edot = strrchr(e, '.');
|
|
const char *ebare = edot ? edot + 1 : e;
|
|
if (strcmp(e, trait_qn) == 0 || strcmp(ebare, tbare) == 0) {
|
|
impls = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!impls)
|
|
continue;
|
|
const CBMRegisteredFunc *mf =
|
|
cbm_registry_lookup_method(reg, t->qualified_name, method_name);
|
|
if (!mf)
|
|
continue;
|
|
if (!first_qn) {
|
|
first = mf;
|
|
first_qn = t->qualified_name;
|
|
n = 1;
|
|
} else if (strcmp(first_qn, t->qualified_name) != 0) {
|
|
n = 2;
|
|
}
|
|
}
|
|
if (out_n)
|
|
*out_n = n;
|
|
if (nm_active && n == 0) {
|
|
cbm_negmemo_insert(&ctx->neg_memo, ctx->arena, nm_key);
|
|
}
|
|
return n == 1 ? first : NULL;
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 8. Macro handling
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Walk a `macro_invocation`'s `token_tree` looking for nested call/method
|
|
* call expressions. We do this so calls inside `vec![foo()]`,
|
|
* `assert_eq!(a.bar(), 0)`, and `dbg!(get_value())` still get attributed
|
|
* to the enclosing function. */
|
|
static void rust_walk_macro_tokens(RustLSPContext *ctx, TSNode node) {
|
|
if (ts_node_is_null(node))
|
|
return;
|
|
const char *kind = ts_node_type(node);
|
|
if (strcmp(kind, "call_expression") == 0 || strcmp(kind, "macro_invocation") == 0 ||
|
|
strcmp(kind, "field_expression") == 0) {
|
|
rust_resolve_calls_in_node(ctx, node);
|
|
return;
|
|
}
|
|
uint32_t nc = ts_node_child_count(node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (!ts_node_is_null(c))
|
|
rust_walk_macro_tokens(ctx, c);
|
|
}
|
|
}
|
|
|
|
/* Map a Rust infix/index operator token to the std::ops trait method that
|
|
* the compiler desugars it to. `a + b` calls `Add::add`, `a[i]` calls
|
|
* `Index::index`, etc. Returns NULL for operators with no overloadable
|
|
* trait method (comparison/logical operators route through PartialEq /
|
|
* PartialOrd whose methods we don't model here — sound to skip). */
|
|
static const char *rust_binop_trait_method(const char *op_text) {
|
|
if (!op_text)
|
|
return NULL;
|
|
if (strcmp(op_text, "+") == 0)
|
|
return "add";
|
|
if (strcmp(op_text, "-") == 0)
|
|
return "sub";
|
|
if (strcmp(op_text, "*") == 0)
|
|
return "mul";
|
|
if (strcmp(op_text, "/") == 0)
|
|
return "div";
|
|
if (strcmp(op_text, "%") == 0)
|
|
return "rem";
|
|
if (strcmp(op_text, "&") == 0)
|
|
return "bitand";
|
|
if (strcmp(op_text, "|") == 0)
|
|
return "bitor";
|
|
if (strcmp(op_text, "^") == 0)
|
|
return "bitxor";
|
|
if (strcmp(op_text, "<<") == 0)
|
|
return "shl";
|
|
if (strcmp(op_text, ">>") == 0)
|
|
return "shr";
|
|
return NULL;
|
|
}
|
|
|
|
/* If `recv` is a user-defined NAMED type that defines operator method
|
|
* `method` (via inherent impl or `impl <trait> for T`), emit a CALLS edge to
|
|
* it. Models Rust operator-overload desugaring (`a + b` → T::add, `a[i]` →
|
|
* T::index). Sound-only: we emit nothing when the operand type is unknown,
|
|
* primitive, or the type has no such method registered — so we never guess on
|
|
* built-in arithmetic. */
|
|
static void rust_emit_operator_call(RustLSPContext *ctx, const CBMType *recv, const char *method) {
|
|
if (!recv || !method)
|
|
return;
|
|
const CBMType *base = recv;
|
|
while (base && (base->kind == CBM_TYPE_REFERENCE || base->kind == CBM_TYPE_POINTER)) {
|
|
base = (base->kind == CBM_TYPE_REFERENCE) ? base->data.reference.elem
|
|
: base->data.pointer.elem;
|
|
}
|
|
/* Only user-defined named types — built-in arithmetic must not emit. */
|
|
if (!base || base->kind != CBM_TYPE_NAMED)
|
|
return;
|
|
const char *type_qn = base->data.named.qualified_name;
|
|
if (!type_qn || is_rust_primitive(type_qn))
|
|
return;
|
|
int impl_count = 0;
|
|
const CBMRegisteredFunc *m = rust_resolve_trait_method(ctx, type_qn, method, &impl_count);
|
|
if (m && m->qualified_name) {
|
|
rust_emit_resolved_call(ctx, m->qualified_name, "lsp_operator_trait",
|
|
CBM_RUST_CONF_OPERATOR);
|
|
/* `a + b` is a binary_expression, never a syntactic call node, so the
|
|
* extractor produced no CBMCall to pair with the resolved_call above.
|
|
* Inject one so the pipeline emits the CALLS edge. */
|
|
rust_inject_syn_call(ctx, m->qualified_name);
|
|
}
|
|
}
|
|
|
|
/* ── User-defined macro_rules! support ────────────────────────────
|
|
*
|
|
* Strategy: collect every `macro_rules!` definition in the file
|
|
* during the pre-walk, store each rule's transcriber text, and on
|
|
* `macro_invocation` re-parse the transcriber as a synthetic Rust
|
|
* function body so any calls inside the body are attributed to the
|
|
* enclosing function of the invocation site.
|
|
*
|
|
* Recursion guard caps expansion depth at 8 (matches the rustc
|
|
* default for macro recursion safety). */
|
|
|
|
typedef struct RustMacroRule {
|
|
const char *macro_name;
|
|
const char *pattern_text; /* left-hand side (without outer brackets) */
|
|
int pattern_len;
|
|
const char *transcriber_text;
|
|
int transcriber_len;
|
|
} RustMacroRule;
|
|
|
|
/* Strip a single outer pair of matching brackets from a token-tree
|
|
* text representation. Returns the inner span via out_text/out_len.
|
|
* If no brackets, returns the original. */
|
|
static void rust_macro_strip_outer(const char *tt, int len, const char **out_text, int *out_len) {
|
|
if (len >= 2 && ((tt[0] == '{' && tt[len - 1] == '}') || (tt[0] == '(' && tt[len - 1] == ')') ||
|
|
(tt[0] == '[' && tt[len - 1] == ']'))) {
|
|
*out_text = tt + 1;
|
|
*out_len = len - 2;
|
|
} else {
|
|
*out_text = tt;
|
|
*out_len = len;
|
|
}
|
|
}
|
|
|
|
static void rust_record_macro_rule(RustLSPContext *ctx, const char *macro_name, TSNode pattern,
|
|
TSNode transcriber) {
|
|
if (!ctx || !macro_name || ts_node_is_null(transcriber))
|
|
return;
|
|
if (ctx->macro_rules_count % 16 == 0) {
|
|
int new_cap = ctx->macro_rules_count + 16;
|
|
struct RustMacroRule **narr = (struct RustMacroRule **)cbm_arena_alloc(
|
|
ctx->arena, new_cap * sizeof(struct RustMacroRule *));
|
|
if (!narr)
|
|
return;
|
|
if (ctx->macro_rules_arr && ctx->macro_rules_count > 0) {
|
|
memcpy(narr, ctx->macro_rules_arr,
|
|
ctx->macro_rules_count * sizeof(struct RustMacroRule *));
|
|
}
|
|
ctx->macro_rules_arr = narr;
|
|
}
|
|
RustMacroRule *r = (RustMacroRule *)cbm_arena_alloc(ctx->arena, sizeof(*r));
|
|
if (!r)
|
|
return;
|
|
memset(r, 0, sizeof(*r));
|
|
r->macro_name = cbm_arena_strdup(ctx->arena, macro_name);
|
|
|
|
/* Cache pattern text for matching at invocation time. */
|
|
if (!ts_node_is_null(pattern)) {
|
|
char *pt = cbm_node_text(ctx->arena, pattern, ctx->source);
|
|
if (pt) {
|
|
const char *inner;
|
|
int inner_len;
|
|
rust_macro_strip_outer(pt, (int)strlen(pt), &inner, &inner_len);
|
|
r->pattern_text = cbm_arena_strndup(ctx->arena, inner, (size_t)inner_len);
|
|
r->pattern_len = inner_len;
|
|
}
|
|
}
|
|
|
|
char *tt = cbm_node_text(ctx->arena, transcriber, ctx->source);
|
|
if (tt) {
|
|
int len = (int)strlen(tt);
|
|
const char *inner;
|
|
int inner_len;
|
|
rust_macro_strip_outer(tt, len, &inner, &inner_len);
|
|
r->transcriber_text = cbm_arena_strndup(ctx->arena, inner, (size_t)inner_len);
|
|
r->transcriber_len = inner_len;
|
|
}
|
|
ctx->macro_rules_arr[ctx->macro_rules_count++] = r;
|
|
}
|
|
|
|
static void rust_collect_macro_rules(RustLSPContext *ctx, TSNode root) {
|
|
if (ts_node_is_null(root))
|
|
return;
|
|
uint32_t nc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(root, i);
|
|
if (ts_node_is_null(c))
|
|
continue;
|
|
const char *k = ts_node_type(c);
|
|
if (strcmp(k, "macro_definition") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(c, "name", 4);
|
|
if (ts_node_is_null(name_node))
|
|
continue;
|
|
char *macro_name = rust_node_text(ctx, name_node);
|
|
if (!macro_name)
|
|
continue;
|
|
uint32_t mnc = ts_node_child_count(c);
|
|
for (uint32_t j = 0; j < mnc; j++) {
|
|
TSNode rule = ts_node_child(c, j);
|
|
if (ts_node_is_null(rule) || !ts_node_is_named(rule))
|
|
continue;
|
|
if (strcmp(ts_node_type(rule), "macro_rule") != 0)
|
|
continue;
|
|
TSNode left = ts_node_child_by_field_name(rule, "left", 4);
|
|
TSNode right = ts_node_child_by_field_name(rule, "right", 5);
|
|
if (!ts_node_is_null(right)) {
|
|
rust_record_macro_rule(ctx, macro_name, left, right);
|
|
}
|
|
}
|
|
} else if (strcmp(k, "mod_item") == 0) {
|
|
TSNode body = ts_node_child_by_field_name(c, "body", 4);
|
|
if (!ts_node_is_null(body))
|
|
rust_collect_macro_rules(ctx, body);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── Metavar matching + substitution ─────────────────────────────
|
|
*
|
|
* Real `macro_rules!` semantics in a clean-room subset. We implement
|
|
* the common shapes:
|
|
* $name:expr / $name:ty / $name:ident / $name:tt / $name:path /
|
|
* $name:pat / $name:literal / $name:block / $name:stmt
|
|
*
|
|
* And one form of repetition: `$(...)<sep><kind>` where kind is `*`,
|
|
* `+`, or `?`. Repetitions inside repetitions are NOT supported.
|
|
*
|
|
* Substitution writes the bound values back into the transcriber and
|
|
* re-parses the result so any calls inside are visible to the
|
|
* resolver. */
|
|
|
|
#define RUST_MACRO_MAX_BINDINGS 32
|
|
#define RUST_MACRO_MAX_REPS 32
|
|
|
|
typedef struct {
|
|
char name[32]; /* metavar name (without `$`) */
|
|
const char *value; /* substring of the invocation args */
|
|
int value_len;
|
|
} MacroBinding;
|
|
|
|
typedef struct {
|
|
MacroBinding kv[RUST_MACRO_MAX_BINDINGS];
|
|
int count;
|
|
/* Repetition bindings: each metavar that appears inside `$(...)*`
|
|
* collects an array of values (one per iteration). */
|
|
struct {
|
|
char name[32];
|
|
const char **values; /* arena-allocated array of strings */
|
|
int *lengths;
|
|
int count;
|
|
} reps[RUST_MACRO_MAX_REPS];
|
|
int rep_count;
|
|
} MacroEnv;
|
|
|
|
/* Skip leading ASCII whitespace and Rust line comments. */
|
|
static int macro_skip_ws(const char *s, int len, int from) {
|
|
while (from < len) {
|
|
char c = s[from];
|
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
|
from++;
|
|
continue;
|
|
}
|
|
if (c == '/' && from + 1 < len && s[from + 1] == '/') {
|
|
from += 2;
|
|
while (from < len && s[from] != '\n')
|
|
from++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return from;
|
|
}
|
|
|
|
/* Consume an identifier (ASCII rust ident). Returns end position or
|
|
* `from` if no identifier. */
|
|
static int macro_consume_ident(const char *s, int len, int from) {
|
|
int p = from;
|
|
if (p >= len)
|
|
return from;
|
|
char c = s[p];
|
|
if (!(c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')))
|
|
return from;
|
|
p++;
|
|
while (p < len) {
|
|
c = s[p];
|
|
if (c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9')) {
|
|
p++;
|
|
} else
|
|
break;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/* Consume a balanced bracket group starting at `from` (which must
|
|
* point at `(`, `[`, or `{`). Returns end position (just after the
|
|
* close bracket) or `from` if not a bracket. */
|
|
static int macro_consume_balanced(const char *s, int len, int from) {
|
|
if (from >= len)
|
|
return from;
|
|
char open = s[from];
|
|
char close = 0;
|
|
if (open == '(')
|
|
close = ')';
|
|
else if (open == '[')
|
|
close = ']';
|
|
else if (open == '{')
|
|
close = '}';
|
|
else
|
|
return from;
|
|
int depth = 1;
|
|
int p = from + 1;
|
|
while (p < len && depth > 0) {
|
|
char c = s[p];
|
|
if (c == '"') {
|
|
/* Skip string literal. */
|
|
p++;
|
|
while (p < len && s[p] != '"') {
|
|
if (s[p] == '\\' && p + 1 < len)
|
|
p += 2;
|
|
else
|
|
p++;
|
|
}
|
|
if (p < len)
|
|
p++;
|
|
continue;
|
|
}
|
|
if (c == '\'') {
|
|
/* Could be lifetime or char literal — accept char. */
|
|
int q = p + 1;
|
|
if (q < len && s[q] == '\\') {
|
|
q++;
|
|
if (q < len)
|
|
q++;
|
|
} else if (q < len)
|
|
q++;
|
|
if (q < len && s[q] == '\'') {
|
|
p = q + 1;
|
|
continue;
|
|
}
|
|
/* otherwise treat as lifetime — skip until non-ident. */
|
|
p = macro_consume_ident(s, len, p + 1);
|
|
continue;
|
|
}
|
|
if (c == '(' || c == '[' || c == '{')
|
|
depth++;
|
|
else if (c == ')' || c == ']' || c == '}')
|
|
depth--;
|
|
p++;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/* Consume a fragment from the input matching the given fragment kind.
|
|
* Returns end position or `from` if the consumption failed. */
|
|
static int macro_consume_fragment(const char *s, int len, int from, const char *frag) {
|
|
from = macro_skip_ws(s, len, from);
|
|
if (from >= len)
|
|
return from;
|
|
if (!frag)
|
|
frag = "tt";
|
|
|
|
if (strcmp(frag, "ident") == 0) {
|
|
return macro_consume_ident(s, len, from);
|
|
}
|
|
if (strcmp(frag, "literal") == 0) {
|
|
/* Numeric, string, char, bool literal. */
|
|
char c = s[from];
|
|
if (c == '"')
|
|
return macro_consume_balanced(s, len, from);
|
|
if (c == '\'') {
|
|
int q = from + 1;
|
|
if (q < len && s[q] == '\\') {
|
|
q++;
|
|
if (q < len)
|
|
q++;
|
|
} else if (q < len)
|
|
q++;
|
|
if (q < len && s[q] == '\'')
|
|
return q + 1;
|
|
return from;
|
|
}
|
|
if (c == '-' || (c >= '0' && c <= '9')) {
|
|
int p = from;
|
|
if (c == '-')
|
|
p++;
|
|
while (p < len && ((s[p] >= '0' && s[p] <= '9') || s[p] == '_' || s[p] == '.' ||
|
|
s[p] == 'x' || s[p] == 'X' || s[p] == 'b' || s[p] == 'o' ||
|
|
(s[p] >= 'a' && s[p] <= 'f') || (s[p] >= 'A' && s[p] <= 'F')))
|
|
p++;
|
|
/* Allow type suffix. */
|
|
int after = macro_consume_ident(s, len, p);
|
|
return after;
|
|
}
|
|
return macro_consume_ident(s, len, from);
|
|
}
|
|
if (strcmp(frag, "tt") == 0) {
|
|
char c = s[from];
|
|
if (c == '(' || c == '[' || c == '{') {
|
|
return macro_consume_balanced(s, len, from);
|
|
}
|
|
/* Single token: identifier, literal, or single-char punct. */
|
|
if (c == '"' || c == '\'') {
|
|
return macro_consume_fragment(s, len, from, "literal");
|
|
}
|
|
if (c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') || c == '-') {
|
|
int p = macro_consume_ident(s, len, from);
|
|
if (p > from)
|
|
return p;
|
|
return macro_consume_fragment(s, len, from, "literal");
|
|
}
|
|
return from + 1;
|
|
}
|
|
/* expr / ty / path / pat / stmt / block — balance brackets, stop
|
|
* at top-level `,` or end of input. */
|
|
if (strcmp(frag, "block") == 0) {
|
|
if (s[from] == '{')
|
|
return macro_consume_balanced(s, len, from);
|
|
return from;
|
|
}
|
|
/* For expr/ty/path/pat/stmt: greedy balanced span. */
|
|
int depth = 0;
|
|
int p = from;
|
|
while (p < len) {
|
|
char c = s[p];
|
|
if (c == '"' || c == '\'') {
|
|
int q = macro_consume_fragment(s, len, p, "literal");
|
|
if (q == p) {
|
|
p++;
|
|
continue;
|
|
}
|
|
p = q;
|
|
continue;
|
|
}
|
|
if (c == '(' || c == '[' || c == '{') {
|
|
int q = macro_consume_balanced(s, len, p);
|
|
if (q == p)
|
|
break;
|
|
p = q;
|
|
continue;
|
|
}
|
|
if (c == ')' || c == ']' || c == '}')
|
|
break;
|
|
if (depth == 0 && c == ',')
|
|
break;
|
|
if (strcmp(frag, "stmt") == 0 && c == ';')
|
|
break;
|
|
p++;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/* Bind a metavar in the env. Returns false if the table is full or the
|
|
* name doesn't fit. */
|
|
static bool macro_env_bind(MacroEnv *env, const char *name, int name_len, const char *val,
|
|
int val_len) {
|
|
if (env->count >= RUST_MACRO_MAX_BINDINGS)
|
|
return false;
|
|
if (name_len <= 0 || name_len >= 32)
|
|
return false;
|
|
MacroBinding *b = &env->kv[env->count++];
|
|
memcpy(b->name, name, name_len);
|
|
b->name[name_len] = '\0';
|
|
b->value = val;
|
|
b->value_len = val_len;
|
|
return true;
|
|
}
|
|
|
|
static const MacroBinding *macro_env_lookup(const MacroEnv *env, const char *name) {
|
|
for (int i = 0; i < env->count; i++) {
|
|
if (strcmp(env->kv[i].name, name) == 0)
|
|
return &env->kv[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Match a pattern against an input. Pattern fragments like `$x:expr`
|
|
* bind into `env`. Returns true if the pattern matches the whole input
|
|
* (or up to the end of pattern, with trailing whitespace in input). */
|
|
static bool macro_pattern_match(const char *pat, int pat_len, const char *in, int in_len,
|
|
MacroEnv *env) {
|
|
int pp = 0; /* pattern pos */
|
|
int ip = 0; /* input pos */
|
|
|
|
while (pp < pat_len) {
|
|
pp = macro_skip_ws(pat, pat_len, pp);
|
|
ip = macro_skip_ws(in, in_len, ip);
|
|
if (pp >= pat_len)
|
|
break;
|
|
|
|
char pc = pat[pp];
|
|
|
|
/* Metavar: $name:frag or $name */
|
|
if (pc == '$') {
|
|
pp++;
|
|
/* Repetition group `$( ... )<sep><kind>` — for now we
|
|
* treat any rep group as a wildcard accepting the rest of
|
|
* the input (best-effort). We bind no names inside reps. */
|
|
pp = macro_skip_ws(pat, pat_len, pp);
|
|
if (pp < pat_len && pat[pp] == '(') {
|
|
int after = macro_consume_balanced(pat, pat_len, pp);
|
|
pp = after;
|
|
/* Skip optional separator + kind (one char each). */
|
|
if (pp < pat_len && pat[pp] != ' ' && pat[pp] != '$' && pat[pp] != '\n') {
|
|
pp++;
|
|
}
|
|
if (pp < pat_len && (pat[pp] == '*' || pat[pp] == '+' || pat[pp] == '?')) {
|
|
pp++;
|
|
}
|
|
/* Consume the rest of the input greedy until we hit
|
|
* the next literal pattern char. */
|
|
int next_lit = pp;
|
|
while (next_lit < pat_len && (pat[next_lit] == ' ' || pat[next_lit] == '\t' ||
|
|
pat[next_lit] == '\n' || pat[next_lit] == '\r')) {
|
|
next_lit++;
|
|
}
|
|
if (next_lit >= pat_len) {
|
|
ip = in_len;
|
|
} else {
|
|
char target = pat[next_lit];
|
|
while (ip < in_len && in[ip] != target)
|
|
ip++;
|
|
}
|
|
continue;
|
|
}
|
|
/* Metavar name. */
|
|
int name_start = pp;
|
|
pp = macro_consume_ident(pat, pat_len, pp);
|
|
int name_end = pp;
|
|
if (name_end == name_start)
|
|
return false;
|
|
const char *frag = "tt";
|
|
if (pp < pat_len && pat[pp] == ':') {
|
|
pp++;
|
|
int frag_start = pp;
|
|
pp = macro_consume_ident(pat, pat_len, pp);
|
|
/* Copy frag into a small buffer. */
|
|
static char frag_buf[16];
|
|
int fl = pp - frag_start;
|
|
if (fl > 0 && fl < (int)sizeof(frag_buf)) {
|
|
memcpy(frag_buf, pat + frag_start, fl);
|
|
frag_buf[fl] = '\0';
|
|
frag = frag_buf;
|
|
}
|
|
}
|
|
/* Consume the fragment from input. */
|
|
int val_start = ip;
|
|
int val_end = macro_consume_fragment(in, in_len, ip, frag);
|
|
if (val_end <= val_start) {
|
|
/* For "expr"/"ty" the matcher may need to accept empty
|
|
* input (e.g. trailing `$($x:expr),*` form). */
|
|
return false;
|
|
}
|
|
ip = val_end;
|
|
if (!macro_env_bind(env, pat + name_start, name_end - name_start, in + val_start,
|
|
val_end - val_start)) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Literal: must match input verbatim. */
|
|
if (ip >= in_len)
|
|
return false;
|
|
char ic = in[ip];
|
|
if (pc != ic)
|
|
return false;
|
|
pp++;
|
|
ip++;
|
|
}
|
|
/* Allow trailing whitespace in input. */
|
|
ip = macro_skip_ws(in, in_len, ip);
|
|
return ip == in_len;
|
|
}
|
|
|
|
/* Substitute env bindings into the transcriber text. Allocates a
|
|
* fresh string in the arena. */
|
|
static char *macro_substitute(CBMArena *arena, const char *xs, int xs_len, const MacroEnv *env) {
|
|
/* Estimate output size: each metavar expansion could be up to
|
|
* ~256 bytes; bound the total at xs_len * 4 + 4KB. */
|
|
int cap = xs_len * 4 + 4096;
|
|
char *out = (char *)cbm_arena_alloc(arena, cap + 1);
|
|
if (!out)
|
|
return NULL;
|
|
int op = 0;
|
|
int xp = 0;
|
|
while (xp < xs_len) {
|
|
char c = xs[xp];
|
|
if (c == '$' && xp + 1 < xs_len) {
|
|
/* Skip rep groups: `$(...)<sep><kind>` — naive expansion:
|
|
* emit the body once, no separator handling. */
|
|
if (xs[xp + 1] == '(') {
|
|
int body_start = xp + 2;
|
|
int after = macro_consume_balanced(xs, xs_len, xp + 1);
|
|
int body_end = after - 1;
|
|
/* Emit the body recursively substituted. */
|
|
char *inner = macro_substitute(arena, xs + body_start, body_end - body_start, env);
|
|
if (inner) {
|
|
int il = (int)strlen(inner);
|
|
if (op + il < cap) {
|
|
memcpy(out + op, inner, il);
|
|
op += il;
|
|
}
|
|
}
|
|
xp = after;
|
|
/* Skip optional separator + kind. */
|
|
if (xp < xs_len && xs[xp] != ' ' && xs[xp] != '\n')
|
|
xp++;
|
|
if (xp < xs_len && (xs[xp] == '*' || xs[xp] == '+' || xs[xp] == '?')) {
|
|
xp++;
|
|
}
|
|
continue;
|
|
}
|
|
/* $name reference. */
|
|
int name_start = xp + 1;
|
|
int name_end = macro_consume_ident(xs, xs_len, name_start);
|
|
if (name_end > name_start) {
|
|
char name_buf[32];
|
|
int nl = name_end - name_start;
|
|
if (nl < 32) {
|
|
memcpy(name_buf, xs + name_start, nl);
|
|
name_buf[nl] = '\0';
|
|
const MacroBinding *b = macro_env_lookup(env, name_buf);
|
|
if (b && b->value && op + b->value_len < cap) {
|
|
memcpy(out + op, b->value, b->value_len);
|
|
op += b->value_len;
|
|
xp = name_end;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (op < cap)
|
|
out[op++] = c;
|
|
xp++;
|
|
}
|
|
out[op] = '\0';
|
|
return out;
|
|
}
|
|
|
|
/* Re-parse a user macro's transcriber body as a synthetic Rust
|
|
* function and walk it for calls. */
|
|
extern const TSLanguage *tree_sitter_rust(void);
|
|
static void rust_expand_user_macro(RustLSPContext *ctx, const char *mname, TSNode invocation) {
|
|
if (!ctx || !mname || !ctx->macro_rules_arr)
|
|
return;
|
|
if (ctx->macro_expand_depth >= 8)
|
|
return;
|
|
|
|
/* Try each rule until one matches; first to match wins. */
|
|
RustMacroRule *hit = NULL;
|
|
MacroEnv env;
|
|
memset(&env, 0, sizeof(env));
|
|
|
|
/* Extract the invocation argument text. */
|
|
const char *inv_args = NULL;
|
|
int inv_args_len = 0;
|
|
if (!ts_node_is_null(invocation)) {
|
|
TSNode args_tt = ts_node_child_by_field_name(invocation, "arguments", 9);
|
|
if (!ts_node_is_null(args_tt)) {
|
|
char *at = cbm_node_text(ctx->arena, args_tt, ctx->source);
|
|
if (at) {
|
|
rust_macro_strip_outer(at, (int)strlen(at), &inv_args, &inv_args_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < ctx->macro_rules_count; i++) {
|
|
RustMacroRule *r = ctx->macro_rules_arr[i];
|
|
if (strcmp(r->macro_name, mname) != 0)
|
|
continue;
|
|
memset(&env, 0, sizeof(env));
|
|
if (r->pattern_text && inv_args &&
|
|
macro_pattern_match(r->pattern_text, r->pattern_len, inv_args, inv_args_len, &env)) {
|
|
hit = r;
|
|
break;
|
|
}
|
|
/* Fall back to first-rule match without arg substitution if
|
|
* the pattern doesn't match — still expand to walk the body. */
|
|
if (!hit)
|
|
hit = r;
|
|
}
|
|
if (!hit || !hit->transcriber_text || hit->transcriber_len <= 0)
|
|
return;
|
|
|
|
/* Substitute the bound metavars into the transcriber body. */
|
|
char *substituted =
|
|
macro_substitute(ctx->arena, hit->transcriber_text, hit->transcriber_len, &env);
|
|
if (!substituted)
|
|
return;
|
|
|
|
/* Expansion memo (see RustLSPContext.macro_memo): reset per top-level
|
|
* invocation, dedup identical (macro, substituted body) within the
|
|
* recursion chain — kills the exponential breadth of self-recursive
|
|
* macro_rules without losing any distinctly-attributed source site. */
|
|
if (ctx->macro_expand_depth == 0 && ctx->macro_memo.slots) {
|
|
memset(ctx->macro_memo.slots, 0, sizeof(uint64_t) * (size_t)ctx->macro_memo.cap);
|
|
ctx->macro_memo.count = 0;
|
|
}
|
|
uint64_t mm_key = cbm_negmemo_key(3, mname, substituted);
|
|
if (cbm_negmemo_contains(&ctx->macro_memo, mm_key)) {
|
|
return;
|
|
}
|
|
cbm_negmemo_insert(&ctx->macro_memo, ctx->arena, mm_key);
|
|
|
|
/* Wrap and parse. */
|
|
char *wrapped = cbm_arena_sprintf(ctx->arena, "fn __cbm_macro_expand() { %s; }\n", substituted);
|
|
if (!wrapped)
|
|
return;
|
|
|
|
TSParser *parser = ts_parser_new();
|
|
if (!parser)
|
|
return;
|
|
ts_parser_set_language(parser, tree_sitter_rust());
|
|
TSTree *tree = ts_parser_parse_string(parser, NULL, wrapped, (uint32_t)strlen(wrapped));
|
|
if (tree) {
|
|
ctx->macro_expand_depth++;
|
|
TSNode root = ts_tree_root_node(tree);
|
|
uint32_t rnc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < rnc; i++) {
|
|
TSNode top = ts_node_child(root, i);
|
|
if (ts_node_is_null(top))
|
|
continue;
|
|
if (strcmp(ts_node_type(top), "function_item") != 0)
|
|
continue;
|
|
TSNode body = ts_node_child_by_field_name(top, "body", 4);
|
|
if (ts_node_is_null(body))
|
|
continue;
|
|
/* Swap source so node_text inside reads from `wrapped`. */
|
|
const char *saved_source = ctx->source;
|
|
int saved_len = ctx->source_len;
|
|
ctx->source = wrapped;
|
|
ctx->source_len = (int)strlen(wrapped);
|
|
rust_resolve_calls_in_node(ctx, body);
|
|
ctx->source = saved_source;
|
|
ctx->source_len = saved_len;
|
|
}
|
|
ctx->macro_expand_depth--;
|
|
ts_tree_delete(tree);
|
|
}
|
|
ts_parser_delete(parser);
|
|
}
|
|
|
|
/* Re-parse the argument list of a built-in expression-macro (format!,
|
|
* println!, assert!, …) as ordinary Rust expressions and walk them for
|
|
* calls. The tree-sitter-rust grammar tokenises macro arguments rather than
|
|
* parsing them as expression AST, so a method call like `format!("{}",
|
|
* d.label())` never appears as a call_expression/field_expression node and
|
|
* rust_walk_macro_tokens cannot recover it. Because these macros DO evaluate
|
|
* their arguments as normal expressions, re-parsing the argument text and
|
|
* resolving calls in it is sound — it recovers exactly the calls the program
|
|
* makes. We wrap the args in a block so each comma-separated argument parses
|
|
* as its own statement; the current scope (params/locals) is preserved so
|
|
* typed receivers still resolve. */
|
|
static void rust_resolve_macro_arg_exprs(RustLSPContext *ctx, TSNode invocation) {
|
|
if (!ctx || ts_node_is_null(invocation))
|
|
return;
|
|
if (ctx->macro_expand_depth >= 8)
|
|
return;
|
|
|
|
/* The grammar exposes the argument list as a `token_tree` child rather
|
|
* than via an `arguments` field, so locate it by node type. */
|
|
TSNode args_tt = {0};
|
|
uint32_t inc = ts_node_child_count(invocation);
|
|
for (uint32_t i = 0; i < inc; i++) {
|
|
TSNode c = ts_node_child(invocation, i);
|
|
if (!ts_node_is_null(c) && strcmp(ts_node_type(c), "token_tree") == 0) {
|
|
args_tt = c;
|
|
break;
|
|
}
|
|
}
|
|
if (ts_node_is_null(args_tt))
|
|
return;
|
|
char *at = cbm_node_text(ctx->arena, args_tt, ctx->source);
|
|
if (!at)
|
|
return;
|
|
const char *inner;
|
|
int inner_len;
|
|
rust_macro_strip_outer(at, (int)strlen(at), &inner, &inner_len);
|
|
if (inner_len <= 0)
|
|
return;
|
|
char *arg_text = cbm_arena_strndup(ctx->arena, inner, (size_t)inner_len);
|
|
if (!arg_text)
|
|
return;
|
|
|
|
/* Same expansion memo as rust_expand_user_macro (site tag 4), but ONLY
|
|
* inside a recursion chain (depth > 0): there the identical argument text
|
|
* is re-parsed on every re-expansion of a self-recursive macro and has no
|
|
* distinct source site. Top-level invocations (depth 0) always parse —
|
|
* identical args in different enclosing functions attribute differently
|
|
* and must all be walked. */
|
|
if (ctx->macro_expand_depth > 0) {
|
|
uint64_t am_key = cbm_negmemo_key(4, arg_text, NULL);
|
|
if (cbm_negmemo_contains(&ctx->macro_memo, am_key)) {
|
|
return;
|
|
}
|
|
cbm_negmemo_insert(&ctx->macro_memo, ctx->arena, am_key);
|
|
}
|
|
|
|
/* Wrap the comma-separated arguments in a tuple expression so the whole
|
|
* thing parses as one valid expression (a trailing format-spec arg like
|
|
* `width = w` would otherwise break statement parsing). Calls inside any
|
|
* element are still walked. */
|
|
char *wrapped =
|
|
cbm_arena_sprintf(ctx->arena, "fn __cbm_macro_args() { let _ = (%s); }\n", arg_text);
|
|
if (!wrapped)
|
|
return;
|
|
|
|
TSParser *parser = ts_parser_new();
|
|
if (!parser)
|
|
return;
|
|
ts_parser_set_language(parser, tree_sitter_rust());
|
|
TSTree *tree = ts_parser_parse_string(parser, NULL, wrapped, (uint32_t)strlen(wrapped));
|
|
if (tree) {
|
|
TSNode root = ts_tree_root_node(tree);
|
|
/* Bail out if the synthetic source failed to parse cleanly — a
|
|
* format-string-only arg, named args, or other non-expression token
|
|
* soup must not produce bogus edges. */
|
|
if (!ts_node_has_error(root)) {
|
|
ctx->macro_expand_depth++;
|
|
uint32_t rnc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < rnc; i++) {
|
|
TSNode top = ts_node_child(root, i);
|
|
if (ts_node_is_null(top))
|
|
continue;
|
|
if (strcmp(ts_node_type(top), "function_item") != 0)
|
|
continue;
|
|
TSNode body = ts_node_child_by_field_name(top, "body", 4);
|
|
if (ts_node_is_null(body))
|
|
continue;
|
|
const char *saved_source = ctx->source;
|
|
int saved_len = ctx->source_len;
|
|
ctx->source = wrapped;
|
|
ctx->source_len = (int)strlen(wrapped);
|
|
/* The syntactic extractor never produced call nodes for these
|
|
* macro-hidden expressions, so resolved_calls emitted here have
|
|
* no CBMCall to pair with. Inject matching synthetic calls. */
|
|
ctx->inject_syn_calls++;
|
|
rust_resolve_calls_in_node(ctx, body);
|
|
ctx->inject_syn_calls--;
|
|
ctx->source = saved_source;
|
|
ctx->source_len = saved_len;
|
|
}
|
|
ctx->macro_expand_depth--;
|
|
}
|
|
ts_tree_delete(tree);
|
|
}
|
|
ts_parser_delete(parser);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 9. Statement / pattern binding
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Recursively bind every identifier inside a pattern node to the given
|
|
* fallback type. Handles tuple_pattern, struct_pattern, tuple_struct_pattern,
|
|
* ref_pattern, mut_pattern, captured_pattern, identifier. */
|
|
static void rust_bind_pattern(RustLSPContext *ctx, TSNode pattern, const CBMType *type) {
|
|
if (ts_node_is_null(pattern))
|
|
return;
|
|
const char *kind = ts_node_type(pattern);
|
|
|
|
if (strcmp(kind, "identifier") == 0) {
|
|
char *name = rust_node_text(ctx, pattern);
|
|
if (name && strcmp(name, "_") != 0) {
|
|
cbm_scope_bind(ctx->current_scope, name, type);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(kind, "captured_pattern") == 0) {
|
|
/* name @ subpattern */
|
|
TSNode name_node = ts_node_child_by_field_name(pattern, "name", 4);
|
|
if (!ts_node_is_null(name_node)) {
|
|
char *name = rust_node_text(ctx, name_node);
|
|
if (name && strcmp(name, "_") != 0) {
|
|
cbm_scope_bind(ctx->current_scope, name, type);
|
|
}
|
|
}
|
|
TSNode sub = ts_node_child_by_field_name(pattern, "pattern", 7);
|
|
if (!ts_node_is_null(sub))
|
|
rust_bind_pattern(ctx, sub, type);
|
|
return;
|
|
}
|
|
if (strcmp(kind, "ref_pattern") == 0 || strcmp(kind, "mut_pattern") == 0 ||
|
|
strcmp(kind, "reference_pattern") == 0) {
|
|
if (ts_node_named_child_count(pattern) > 0) {
|
|
rust_bind_pattern(ctx, ts_node_named_child(pattern, 0), type);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(kind, "tuple_pattern") == 0) {
|
|
const CBMType *base = type;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE)
|
|
base = base->data.reference.elem;
|
|
uint32_t nc = ts_node_named_child_count(pattern);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
const CBMType *elem_t = cbm_type_unknown();
|
|
if (base && base->kind == CBM_TYPE_TUPLE && (int)i < base->data.tuple.count) {
|
|
elem_t = base->data.tuple.elems[i];
|
|
}
|
|
rust_bind_pattern(ctx, ts_node_named_child(pattern, i), elem_t);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(kind, "tuple_struct_pattern") == 0) {
|
|
/* Some(x), Ok(x), Err(e) — peel one Option/Result/template. */
|
|
const CBMType *base = type;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE)
|
|
base = base->data.reference.elem;
|
|
const CBMType *inner = cbm_type_unknown();
|
|
if (base && base->kind == CBM_TYPE_TEMPLATE && base->data.template_type.arg_count > 0) {
|
|
inner = base->data.template_type.template_args[0];
|
|
}
|
|
uint32_t nc = ts_node_named_child_count(pattern);
|
|
/* First named child is the path; subsequent are sub-patterns. */
|
|
for (uint32_t i = 1; i < nc; i++) {
|
|
rust_bind_pattern(ctx, ts_node_named_child(pattern, i), inner);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(kind, "struct_pattern") == 0) {
|
|
/* For each field_pattern, bind the local name to the field's type. */
|
|
TSNode body = ts_node_child_by_field_name(pattern, "body", 4);
|
|
TSNode iter = ts_node_is_null(body) ? pattern : body;
|
|
const CBMType *base = type;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE)
|
|
base = base->data.reference.elem;
|
|
const char *type_qn = NULL;
|
|
if (base && base->kind == CBM_TYPE_NAMED)
|
|
type_qn = base->data.named.qualified_name;
|
|
else if (base && base->kind == CBM_TYPE_TEMPLATE)
|
|
type_qn = base->data.template_type.template_name;
|
|
|
|
uint32_t nc = ts_node_named_child_count(iter);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode fp = ts_node_named_child(iter, i);
|
|
const char *fk = ts_node_type(fp);
|
|
if (strcmp(fk, "field_pattern") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(fp, "name", 4);
|
|
TSNode pat_node = ts_node_child_by_field_name(fp, "pattern", 7);
|
|
char *fname = rust_node_text(ctx, name_node);
|
|
if (!fname)
|
|
continue;
|
|
const CBMType *ft =
|
|
type_qn ? rust_lookup_field(ctx, type_qn, fname, 0) : cbm_type_unknown();
|
|
if (!ft)
|
|
ft = cbm_type_unknown();
|
|
if (!ts_node_is_null(pat_node)) {
|
|
rust_bind_pattern(ctx, pat_node, ft);
|
|
} else {
|
|
cbm_scope_bind(ctx->current_scope, fname, ft);
|
|
}
|
|
} else if (strcmp(fk, "shorthand_field_identifier") == 0 ||
|
|
strcmp(fk, "identifier") == 0) {
|
|
char *fname = rust_node_text(ctx, fp);
|
|
if (fname && strcmp(fname, "_") != 0) {
|
|
const CBMType *ft =
|
|
type_qn ? rust_lookup_field(ctx, type_qn, fname, 0) : cbm_type_unknown();
|
|
cbm_scope_bind(ctx->current_scope, fname, ft ? ft : cbm_type_unknown());
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(kind, "or_pattern") == 0) {
|
|
/* For an OR pattern we attempt to bind names from the first branch. */
|
|
if (ts_node_named_child_count(pattern) > 0) {
|
|
rust_bind_pattern(ctx, ts_node_named_child(pattern, 0), type);
|
|
}
|
|
return;
|
|
}
|
|
/* Other patterns we ignore for binding purposes. */
|
|
}
|
|
|
|
void rust_process_statement(RustLSPContext *ctx, TSNode node) {
|
|
if (ts_node_is_null(node))
|
|
return;
|
|
const char *kind = ts_node_type(node);
|
|
|
|
/* let_declaration: let pat: T = expr;
|
|
*
|
|
* Bidirectional inference: if the user wrote `let v: Vec<String> = …;`
|
|
* we pass `Vec<String>` as the expected hint when synthesising the
|
|
* RHS. That lets ambiguous calls like `Vec::new()` keep their full
|
|
* template arguments through the chain. */
|
|
if (strcmp(kind, "let_declaration") == 0) {
|
|
TSNode pat = ts_node_child_by_field_name(node, "pattern", 7);
|
|
TSNode tn = ts_node_child_by_field_name(node, "type", 4);
|
|
TSNode val = ts_node_child_by_field_name(node, "value", 5);
|
|
|
|
const CBMType *annotated = NULL;
|
|
if (!ts_node_is_null(tn)) {
|
|
annotated = rust_parse_type_node(ctx, tn);
|
|
}
|
|
const CBMType *let_type = annotated;
|
|
if ((!let_type || cbm_type_is_unknown(let_type)) && !ts_node_is_null(val)) {
|
|
/* Synthesis: evaluate the RHS with the (possibly NULL)
|
|
* annotated type as a hint. */
|
|
let_type = rust_eval_expr_typed(ctx, val, annotated);
|
|
}
|
|
if (!let_type)
|
|
let_type = cbm_type_unknown();
|
|
if (!ts_node_is_null(pat))
|
|
rust_bind_pattern(ctx, pat, let_type);
|
|
return;
|
|
}
|
|
|
|
/* const_item / static_item: const NAME: T = …; */
|
|
if (strcmp(kind, "const_item") == 0 || strcmp(kind, "static_item") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
|
|
TSNode tn = ts_node_child_by_field_name(node, "type", 4);
|
|
const CBMType *type =
|
|
ts_node_is_null(tn) ? cbm_type_unknown() : rust_parse_type_node(ctx, tn);
|
|
if (!ts_node_is_null(name_node)) {
|
|
char *name = rust_node_text(ctx, name_node);
|
|
if (name)
|
|
cbm_scope_bind(ctx->current_scope, name, type);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 10. Function & file walk
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Inject a synthetic CBMCall into result->calls so the downstream pipeline
|
|
* (cbm_pipeline_find_lsp_resolution) can pair it with the resolved_call and
|
|
* emit a CALLS edge. `callee_qn`'s last dot segment is used as the textual
|
|
* callee_name, matching how the resolver's short-name comparison works. Only
|
|
* used for calls the syntactic extractor cannot see (operator desugaring,
|
|
* macro-hidden method calls). */
|
|
static void rust_inject_syn_call(RustLSPContext *ctx, const char *callee_qn) {
|
|
if (!ctx || !ctx->syn_calls || !callee_qn || !ctx->enclosing_func_qn)
|
|
return;
|
|
const char *dot = strrchr(callee_qn, '.');
|
|
const char *short_name = dot ? dot + 1 : callee_qn;
|
|
if (!short_name || !short_name[0])
|
|
return;
|
|
CBMCall call = {0};
|
|
call.callee_name = cbm_arena_strdup(ctx->arena, short_name);
|
|
call.enclosing_func_qn = ctx->enclosing_func_qn;
|
|
cbm_calls_push(ctx->syn_calls, ctx->arena, call);
|
|
}
|
|
|
|
static void rust_emit_resolved_call(RustLSPContext *ctx, const char *callee_qn,
|
|
const char *strategy, float confidence) {
|
|
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
|
|
return;
|
|
CBMResolvedCall rc = {
|
|
.caller_qn = ctx->enclosing_func_qn,
|
|
.callee_qn = callee_qn,
|
|
.strategy = strategy,
|
|
.confidence = confidence,
|
|
.reason = NULL,
|
|
};
|
|
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
|
if (ctx->inject_syn_calls > 0) {
|
|
rust_inject_syn_call(ctx, callee_qn);
|
|
}
|
|
}
|
|
|
|
static void rust_emit_unresolved_call(RustLSPContext *ctx, const char *expr_text,
|
|
const char *reason) {
|
|
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn)
|
|
return;
|
|
CBMResolvedCall rc = {
|
|
.caller_qn = ctx->enclosing_func_qn,
|
|
.callee_qn = expr_text ? expr_text : "?",
|
|
.strategy = "lsp_unresolved",
|
|
.confidence = 0.0f,
|
|
.reason = reason,
|
|
};
|
|
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
|
}
|
|
|
|
/* Entry hook: classify a call_expression and emit the best edge we can. */
|
|
static void rust_resolve_call_expression(RustLSPContext *ctx, TSNode node) {
|
|
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
|
|
TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9);
|
|
if (ts_node_is_null(func_node))
|
|
return;
|
|
const char *fk = ts_node_type(func_node);
|
|
|
|
/* Method call via field_expression. The grammar can also expose
|
|
* `s.cast::<T>(x)` as a `generic_function` callee whose inner
|
|
* `function` is a field_expression — peel that wrapper here. */
|
|
if (strcmp(fk, "generic_function") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(func_node, "function", 8);
|
|
if (!ts_node_is_null(inner)) {
|
|
func_node = inner;
|
|
fk = ts_node_type(func_node);
|
|
}
|
|
}
|
|
if (strcmp(fk, "field_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(func_node, "value", 5);
|
|
TSNode field = ts_node_child_by_field_name(func_node, "field", 5);
|
|
if (ts_node_is_null(value) || ts_node_is_null(field))
|
|
return;
|
|
char *mname = rust_node_text(ctx, field);
|
|
if (!mname)
|
|
return;
|
|
/* Strip turbofish from the method name in case the grammar
|
|
* embedded it as part of the field token. */
|
|
rust_strip_turbofish(mname);
|
|
|
|
const CBMType *recv = rust_eval_expr_type(ctx, value);
|
|
const CBMType *base = recv;
|
|
while (base && (base->kind == CBM_TYPE_REFERENCE || base->kind == CBM_TYPE_POINTER)) {
|
|
base = (base->kind == CBM_TYPE_REFERENCE) ? base->data.reference.elem
|
|
: base->data.pointer.elem;
|
|
}
|
|
|
|
const char *type_qn = NULL;
|
|
if (base && base->kind == CBM_TYPE_NAMED)
|
|
type_qn = base->data.named.qualified_name;
|
|
else if (base && base->kind == CBM_TYPE_TEMPLATE)
|
|
type_qn = base->data.template_type.template_name;
|
|
else if (base && base->kind == CBM_TYPE_BUILTIN) {
|
|
const char *nm = base->data.builtin.name;
|
|
if (strcmp(nm, "str") == 0)
|
|
type_qn = "core.str";
|
|
else if (strcmp(nm, "String") == 0)
|
|
type_qn = "alloc.string.String";
|
|
else if (is_rust_primitive(nm)) {
|
|
/* Primitive integer / float / char / bool / unit / never
|
|
* methods are registered under their primitive name. */
|
|
type_qn = nm;
|
|
}
|
|
} else if (base && base->kind == CBM_TYPE_SLICE) {
|
|
/* `&[T]` / `[T]` method dispatch. */
|
|
type_qn = "core.slice";
|
|
}
|
|
|
|
if (type_qn) {
|
|
int impl_count = 0;
|
|
const CBMRegisteredFunc *m =
|
|
rust_resolve_trait_method(ctx, type_qn, mname, &impl_count);
|
|
if (m) {
|
|
const char *strategy = "lsp_method_dispatch";
|
|
float conf = CBM_RUST_CONF_METHOD;
|
|
if (m->receiver_type && strcmp(m->receiver_type, type_qn) != 0) {
|
|
strategy = "lsp_trait_dispatch";
|
|
conf = (impl_count == 1) ? CBM_RUST_CONF_TRAIT_SOLE : CBM_RUST_CONF_TRAIT_AMB;
|
|
} else if (rust_method_is_trait_impl(ctx, type_qn, mname)) {
|
|
// Inherently resolved, but the method comes from a trait impl
|
|
// (`impl Trait for Type`) → polymorphic trait dispatch.
|
|
strategy = "lsp_trait_dispatch";
|
|
conf = CBM_RUST_CONF_TRAIT_SOLE;
|
|
}
|
|
rust_emit_resolved_call(ctx, m->qualified_name, strategy, conf);
|
|
(void)args_node;
|
|
return;
|
|
}
|
|
|
|
/* Walk the Deref chain — `Box<T>::method` may live on `T`,
|
|
* `Rc<RefCell<T>>::method` peels two levels, etc. We bound
|
|
* the chain at 8 hops to mirror the rust-analyzer cap. */
|
|
const CBMType *cur = base;
|
|
for (int hop = 0; hop < 8; hop++) {
|
|
const CBMType *next = rust_deref_step(ctx, cur);
|
|
if (!next)
|
|
break;
|
|
/* Unwrap reference layers introduced by deref. */
|
|
while (next &&
|
|
(next->kind == CBM_TYPE_REFERENCE || next->kind == CBM_TYPE_POINTER)) {
|
|
next = (next->kind == CBM_TYPE_REFERENCE) ? next->data.reference.elem
|
|
: next->data.pointer.elem;
|
|
}
|
|
if (!next)
|
|
break;
|
|
const char *next_qn = NULL;
|
|
if (next->kind == CBM_TYPE_NAMED)
|
|
next_qn = next->data.named.qualified_name;
|
|
else if (next->kind == CBM_TYPE_TEMPLATE)
|
|
next_qn = next->data.template_type.template_name;
|
|
else if (next->kind == CBM_TYPE_BUILTIN) {
|
|
const char *nm = next->data.builtin.name;
|
|
if (strcmp(nm, "str") == 0)
|
|
next_qn = "core.str";
|
|
else if (strcmp(nm, "String") == 0)
|
|
next_qn = "alloc.string.String";
|
|
}
|
|
if (!next_qn)
|
|
break;
|
|
int hop_impls = 0;
|
|
const CBMRegisteredFunc *hm =
|
|
rust_resolve_trait_method(ctx, next_qn, mname, &hop_impls);
|
|
if (hm) {
|
|
rust_emit_resolved_call(ctx, hm->qualified_name, "lsp_deref_dispatch",
|
|
CBM_RUST_CONF_PROMOTED);
|
|
return;
|
|
}
|
|
cur = next;
|
|
}
|
|
|
|
/* Chalk-lite: receiver is typed as a NAMED with a name
|
|
* that matches a current type-param bound. Resolve through
|
|
* the bound trait. */
|
|
{
|
|
/* Just the local tail name. */
|
|
const char *short_qn = type_qn;
|
|
const char *dot = strrchr(type_qn, '.');
|
|
if (dot)
|
|
short_qn = dot + 1;
|
|
const char *bound = rust_lookup_type_param_bound(ctx, short_qn);
|
|
if (bound) {
|
|
int bimpls = 0;
|
|
const CBMRegisteredFunc *bm =
|
|
rust_resolve_trait_method(ctx, bound, mname, &bimpls);
|
|
if (bm) {
|
|
rust_emit_resolved_call(ctx, bm->qualified_name, "lsp_bound_dispatch",
|
|
CBM_RUST_CONF_TRAIT_AMB);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Prelude trait method best-effort. */
|
|
if (is_prelude_trait_method(mname)) {
|
|
rust_emit_resolved_call(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
|
|
"lsp_prelude_trait", CBM_RUST_CONF_TRAIT_AMB);
|
|
return;
|
|
}
|
|
rust_emit_unresolved_call(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
|
|
"method_not_found");
|
|
return;
|
|
}
|
|
|
|
/* Receiver type is unknown — record best-effort with the textual
|
|
* receiver path so downstream can still see what we tried. */
|
|
char *recv_text = rust_node_text(ctx, value);
|
|
rust_emit_unresolved_call(
|
|
ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_text ? recv_text : "?", mname),
|
|
"unknown_receiver_type");
|
|
return;
|
|
}
|
|
|
|
/* Direct identifier or scoped path call. */
|
|
if (strcmp(fk, "identifier") == 0 || strcmp(fk, "scoped_identifier") == 0 ||
|
|
strcmp(fk, "generic_function") == 0) {
|
|
TSNode actual_func = func_node;
|
|
if (strcmp(fk, "generic_function") == 0) {
|
|
TSNode inner = ts_node_child_by_field_name(actual_func, "function", 8);
|
|
if (!ts_node_is_null(inner))
|
|
actual_func = inner;
|
|
}
|
|
char *path = rust_node_text(ctx, actual_func);
|
|
if (!path)
|
|
return;
|
|
/* Strip ALL turbofish (`Vec::<i32>::new` → `Vec::new`). */
|
|
rust_strip_turbofish(path);
|
|
|
|
const char *qn = rust_resolve_path_expr(ctx, path);
|
|
if (!qn)
|
|
return;
|
|
|
|
/* Try registered free function first. Also try module-prefixed
|
|
* fallback so `Logger::new` (which resolves to "Logger.new")
|
|
* still finds the project's `<module>.Logger.new`. */
|
|
if (cbm_registry_lookup_func(ctx->registry, qn)) {
|
|
rust_emit_resolved_call(ctx, qn, "lsp_direct", CBM_RUST_CONF_DIRECT);
|
|
return;
|
|
}
|
|
if (ctx->module_qn && strstr(qn, ".") == NULL) {
|
|
const char *full = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, qn);
|
|
if (cbm_registry_lookup_func(ctx->registry, full)) {
|
|
rust_emit_resolved_call(ctx, full, "lsp_direct", CBM_RUST_CONF_DIRECT);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* UFCS form: T::method or trait_qn::method. */
|
|
const char *dot = strrchr(qn, '.');
|
|
if (dot) {
|
|
char *head = cbm_arena_strndup(ctx->arena, qn, (size_t)(dot - qn));
|
|
const char *short_name = dot + 1;
|
|
/* If `head` is a trait, `Trait::method` UFCS resolves to the sole
|
|
* concrete impl (lsp_trait_ufcs), NEVER the trait's own abstract
|
|
* method that the inherent lookup below would find. Resolve the trait
|
|
* QN (head or module-qualified) via its is_interface flag — set at
|
|
* type-registration time, so it is reliable even on an early pass
|
|
* before impl links are wired. When the impl isn't known yet, emit
|
|
* nothing: a partial-pass lsp_ufcs to the abstract method would
|
|
* otherwise outrank (higher conf) the real trait_ufcs from the
|
|
* complete pass and win the join. */
|
|
const char *trait_qn = NULL;
|
|
const CBMRegisteredType *head_t = cbm_registry_lookup_type(ctx->registry, head);
|
|
if (head_t && head_t->is_interface) {
|
|
trait_qn = head;
|
|
} else if (ctx->module_qn) {
|
|
const char *fh = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, head);
|
|
const CBMRegisteredType *ft = cbm_registry_lookup_type(ctx->registry, fh);
|
|
if (ft && ft->is_interface)
|
|
trait_qn = fh;
|
|
}
|
|
if (trait_qn) {
|
|
int tn = 0;
|
|
const CBMRegisteredFunc *ti_m =
|
|
rust_find_sole_trait_impl(ctx, trait_qn, short_name, &tn);
|
|
if (tn >= 1) {
|
|
rust_emit_resolved_call(
|
|
ctx,
|
|
ti_m ? ti_m->qualified_name
|
|
: cbm_arena_sprintf(ctx->arena, "%s.%s", trait_qn, short_name),
|
|
tn == 1 ? "lsp_trait_ufcs" : "lsp_trait_ufcs_amb",
|
|
tn == 1 ? CBM_RUST_CONF_TRAIT_SOLE : CBM_RUST_CONF_TRAIT_AMB);
|
|
}
|
|
return;
|
|
}
|
|
const CBMRegisteredFunc *m =
|
|
cbm_registry_lookup_method_aliased(ctx->registry, head, short_name);
|
|
if (!m && ctx->module_qn) {
|
|
/* Fall back to module-qualified head: `Logger.new` →
|
|
* `<module>.Logger.new`. */
|
|
const char *full_head =
|
|
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, head);
|
|
m = cbm_registry_lookup_method_aliased(ctx->registry, full_head, short_name);
|
|
}
|
|
if (m) {
|
|
rust_emit_resolved_call(ctx, m->qualified_name,
|
|
strcmp(short_name, "new") == 0 ? "lsp_constructor"
|
|
: "lsp_ufcs",
|
|
CBM_RUST_CONF_UFCS);
|
|
return;
|
|
}
|
|
/* Trait method through single-impl dispatch. */
|
|
int impls = 0;
|
|
const CBMRegisteredFunc *tm = rust_resolve_trait_method(ctx, head, short_name, &impls);
|
|
if (!tm && ctx->module_qn) {
|
|
const char *full_head =
|
|
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, head);
|
|
tm = rust_resolve_trait_method(ctx, full_head, short_name, &impls);
|
|
}
|
|
if (tm) {
|
|
rust_emit_resolved_call(
|
|
ctx, tm->qualified_name, impls == 1 ? "lsp_trait_ufcs" : "lsp_trait_ufcs_amb",
|
|
impls == 1 ? CBM_RUST_CONF_TRAIT_SOLE : CBM_RUST_CONF_TRAIT_AMB);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const char *tail = strrchr(path, ':');
|
|
if (tail && tail > path && tail[-1] == ':') {
|
|
tail += 1;
|
|
} else {
|
|
tail = path;
|
|
}
|
|
|
|
/* Cross-crate workspace-member resolution (#56): when the call
|
|
* path's head is a declared Cargo workspace member (e.g.
|
|
* `crate_a::helper` from inside crate_b) we cannot rely on the
|
|
* caller-crate-scoped fallback below — that filters by the
|
|
* CALLER's module prefix and would resolve to a same-named local
|
|
* function instead. Route to the function defined inside the
|
|
* MEMBER crate by matching the registered QN's `.<member>.`
|
|
* path segment plus the call tail. Requires a parsed manifest
|
|
* (threaded through pass_lsp_cross.c); NULL manifest skips this. */
|
|
if (ctx->cargo_manifest && tail && *tail) {
|
|
const char *head_sep = strstr(path, "::");
|
|
if (head_sep && head_sep > path) {
|
|
char *head = cbm_arena_strndup(ctx->arena, path, (size_t)(head_sep - path));
|
|
const CBMCargoManifest *m = (const CBMCargoManifest *)ctx->cargo_manifest;
|
|
if (head && cbm_cargo_find_member(m, head)) {
|
|
/* `.crate_a.` — the member directory appears as a dotted
|
|
* QN segment for every def inside that crate. */
|
|
char *needle = cbm_arena_sprintf(ctx->arena, ".%s.", head);
|
|
const CBMRegisteredFunc *mem_unique = NULL;
|
|
int mem_matches = 0;
|
|
/* Iterate only free funcs whose short_name == tail via the index;
|
|
* the receiver/short_name/needle re-checks below are unchanged. */
|
|
CBMFreeFuncIter ffit;
|
|
cbm_registry_free_funcs_by_short_name(ctx->registry, tail, &ffit);
|
|
for (int i; mem_matches < 2 && (i = cbm_free_func_iter_next(&ffit)) >= 0;) {
|
|
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
|
|
if (!f->short_name || !f->qualified_name)
|
|
continue;
|
|
if (f->receiver_type)
|
|
continue; /* free functions only */
|
|
if (strcmp(f->short_name, tail) != 0)
|
|
continue;
|
|
if (!strstr(f->qualified_name, needle))
|
|
continue; /* not defined in the member crate */
|
|
mem_matches++;
|
|
if (mem_matches == 1)
|
|
mem_unique = f;
|
|
}
|
|
if (mem_matches == 1 && mem_unique) {
|
|
rust_emit_resolved_call(ctx, mem_unique->qualified_name, "lsp_cross_crate",
|
|
CBM_RUST_CONF_DIRECT);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Global short-name fallback: scan the registry for a unique
|
|
* function whose short_name matches the path's tail and whose
|
|
* QN starts with the current crate prefix. This gives `mod
|
|
* foo; use foo::bar; bar()` a chance to resolve when the
|
|
* intermediate module wasn't tracked through an explicit
|
|
* use-map entry. */
|
|
if (tail && *tail && ctx->module_qn) {
|
|
/* Crate prefix is the first dotted segment of module_qn after
|
|
* the project name, but for simplicity we just match on
|
|
* "starts with first dot-segment". */
|
|
const char *first_dot = strchr(ctx->module_qn, '.');
|
|
size_t crate_len =
|
|
first_dot ? (size_t)(first_dot - ctx->module_qn) : strlen(ctx->module_qn);
|
|
const CBMRegisteredFunc *unique = NULL;
|
|
int matches = 0;
|
|
/* Iterate only free funcs whose short_name == tail via the index; the
|
|
* receiver/short_name/crate-prefix re-checks below are unchanged. */
|
|
CBMFreeFuncIter ffit;
|
|
cbm_registry_free_funcs_by_short_name(ctx->registry, tail, &ffit);
|
|
for (int i; matches < 2 && (i = cbm_free_func_iter_next(&ffit)) >= 0;) {
|
|
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
|
|
if (!f->short_name || !f->qualified_name)
|
|
continue;
|
|
if (f->receiver_type)
|
|
continue; /* free functions only */
|
|
if (strcmp(f->short_name, tail) != 0)
|
|
continue;
|
|
/* Crate-scoped: QN must start with the same prefix. */
|
|
if (strncmp(f->qualified_name, ctx->module_qn, crate_len) != 0)
|
|
continue;
|
|
matches++;
|
|
if (matches == 1)
|
|
unique = f;
|
|
}
|
|
if (matches == 1 && unique) {
|
|
rust_emit_resolved_call(ctx, unique->qualified_name, "lsp_short_name_unique",
|
|
CBM_RUST_CONF_PROMOTED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Last-ditch: emit with the resolved path. */
|
|
rust_emit_unresolved_call(ctx, qn, "function_not_in_registry");
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Walk every node in a function body, recording calls and refining scope
|
|
* for control-flow constructs that bind variables. */
|
|
#define CBM_RUST_EVAL_STEP_CAP 200000 /* per-file budget */
|
|
static void rust_resolve_calls_in_node(RustLSPContext *ctx, TSNode node) {
|
|
if (ts_node_is_null(node))
|
|
return;
|
|
/* Pathological-input guard: bail out once we've spent too many
|
|
* eval steps on this file. Prevents hangs on adversarial input. */
|
|
if (ctx->eval_step_count > CBM_RUST_EVAL_STEP_CAP)
|
|
return;
|
|
ctx->eval_step_count++;
|
|
const char *kind = ts_node_type(node);
|
|
|
|
/* Bind variables introduced by this statement. */
|
|
rust_process_statement(ctx, node);
|
|
|
|
/* Resolve a call expression. */
|
|
if (strcmp(kind, "call_expression") == 0) {
|
|
rust_resolve_call_expression(ctx, node);
|
|
/* Closure-parameter inference: when the call is a known
|
|
* iterator-style method that takes a closure of `Item`, stash
|
|
* the receiver's element type so the closure_expression child
|
|
* binds its first param to it. We compute this here (after
|
|
* the call edge is emitted) so the recursion picks it up. */
|
|
TSNode fn = ts_node_child_by_field_name(node, "function", 8);
|
|
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "field_expression") == 0) {
|
|
TSNode val = ts_node_child_by_field_name(fn, "value", 5);
|
|
TSNode fld = ts_node_child_by_field_name(fn, "field", 5);
|
|
if (!ts_node_is_null(val) && !ts_node_is_null(fld)) {
|
|
char *mname = rust_node_text(ctx, fld);
|
|
static const char *item_methods[] = {
|
|
"map", "filter", "for_each", "find", "position", "any",
|
|
"all", "take_while", "skip_while", "filter_map", "inspect", "max_by",
|
|
"min_by", "max_by_key", "min_by_key", "sort_by_key", NULL};
|
|
bool is_item_method = false;
|
|
if (mname) {
|
|
for (const char **mm = item_methods; *mm; mm++) {
|
|
if (strcmp(mname, *mm) == 0) {
|
|
is_item_method = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (is_item_method) {
|
|
const CBMType *recv = rust_eval_expr_type(ctx, val);
|
|
const CBMType *base = recv;
|
|
while (base &&
|
|
(base->kind == CBM_TYPE_REFERENCE || base->kind == CBM_TYPE_POINTER)) {
|
|
base = (base->kind == CBM_TYPE_REFERENCE) ? base->data.reference.elem
|
|
: base->data.pointer.elem;
|
|
}
|
|
if (base && base->kind == CBM_TYPE_TEMPLATE &&
|
|
base->data.template_type.arg_count > 0) {
|
|
const char *tn = base->data.template_type.template_name;
|
|
if (tn && (strstr(tn, "Iterator") || strstr(tn, "Vec") ||
|
|
strstr(tn, "VecDeque") || strstr(tn, "Option") ||
|
|
strstr(tn, "Result") || strstr(tn, "HashSet") ||
|
|
strstr(tn, "BTreeSet") || strstr(tn, "Slice"))) {
|
|
ctx->pending_closure_param_type =
|
|
base->data.template_type.template_args[0];
|
|
}
|
|
} else if (base && base->kind == CBM_TYPE_SLICE) {
|
|
ctx->pending_closure_param_type = base->data.slice.elem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Continue recursion so calls inside arguments are also seen. */
|
|
}
|
|
|
|
/* Operator-overload desugaring: `a + b` calls <T as Add>::add when the
|
|
* left operand is a user-defined type T with that operator method;
|
|
* `a[i]` calls T::index. The tree-sitter-rust grammar models these as
|
|
* binary_expression / index_expression rather than call_expression, so
|
|
* lang_specs.c's call-type whitelist never sees them — we recover the
|
|
* call edge here. Sound-only via rust_emit_operator_call (no edge unless
|
|
* the operand type actually defines the method). */
|
|
if (strcmp(kind, "binary_expression") == 0) {
|
|
TSNode left = ts_node_child_by_field_name(node, "left", 4);
|
|
if (!ts_node_is_null(left)) {
|
|
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (ts_node_is_named(c))
|
|
continue;
|
|
char *op = rust_node_text(ctx, c);
|
|
const char *method = rust_binop_trait_method(op);
|
|
if (method) {
|
|
rust_emit_operator_call(ctx, rust_eval_expr_type(ctx, left), method);
|
|
}
|
|
break; /* operator is the sole anonymous child */
|
|
}
|
|
}
|
|
} else if (strcmp(kind, "index_expression") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(node, "value", 5);
|
|
if (ts_node_is_null(value) && ts_node_named_child_count(node) > 0) {
|
|
value = ts_node_named_child(node, 0);
|
|
}
|
|
if (!ts_node_is_null(value)) {
|
|
rust_emit_operator_call(ctx, rust_eval_expr_type(ctx, value), "index");
|
|
}
|
|
}
|
|
|
|
/* Macro invocation: walk inner tokens for nested calls and try the
|
|
* macro-as-function mapping. */
|
|
if (strcmp(kind, "macro_invocation") == 0) {
|
|
TSNode mname_node = ts_node_child_by_field_name(node, "macro", 5);
|
|
if (!ts_node_is_null(mname_node)) {
|
|
char *mname = rust_node_text(ctx, mname_node);
|
|
if (mname) {
|
|
/* For known std macros emit a synthetic call under their
|
|
* canonical paths so trace tools can see the dependency. */
|
|
const char *path = NULL;
|
|
/* Macros whose arguments are ordinary Rust expressions — their
|
|
* call sites are lost to tree-sitter's macro tokenisation, so
|
|
* re-parse the args to recover calls like `format!("{}",
|
|
* d.label())`. */
|
|
bool expr_arg_macro =
|
|
strcmp(mname, "println") == 0 || strcmp(mname, "eprintln") == 0 ||
|
|
strcmp(mname, "print") == 0 || strcmp(mname, "eprint") == 0 ||
|
|
strcmp(mname, "format") == 0 || strcmp(mname, "write") == 0 ||
|
|
strcmp(mname, "writeln") == 0 || strcmp(mname, "panic") == 0 ||
|
|
strcmp(mname, "assert") == 0 || strcmp(mname, "assert_eq") == 0 ||
|
|
strcmp(mname, "assert_ne") == 0 || strcmp(mname, "debug_assert") == 0 ||
|
|
strcmp(mname, "debug_assert_eq") == 0 ||
|
|
strcmp(mname, "debug_assert_ne") == 0 || strcmp(mname, "dbg") == 0;
|
|
if (expr_arg_macro) {
|
|
rust_resolve_macro_arg_exprs(ctx, node);
|
|
}
|
|
if (strcmp(mname, "println") == 0 || strcmp(mname, "eprintln") == 0 ||
|
|
strcmp(mname, "print") == 0 || strcmp(mname, "eprint") == 0 ||
|
|
strcmp(mname, "format") == 0 || strcmp(mname, "write") == 0 ||
|
|
strcmp(mname, "writeln") == 0) {
|
|
path = cbm_arena_sprintf(ctx->arena, "std.macros.%s", mname);
|
|
} else if (strcmp(mname, "vec") == 0) {
|
|
path = "alloc.vec.vec";
|
|
} else if (strcmp(mname, "panic") == 0) {
|
|
path = "core.panicking.panic";
|
|
} else if (strcmp(mname, "include") == 0) {
|
|
/* `include!` pulls another file in at compile time —
|
|
* we never see the included source. Emit a
|
|
* documentation edge so trace tools can flag it. */
|
|
path = "core.macros.include";
|
|
} else if (strcmp(mname, "include_str") == 0 ||
|
|
strcmp(mname, "include_bytes") == 0) {
|
|
/* Equivalent for data inclusion. */
|
|
path = cbm_arena_sprintf(ctx->arena, "core.macros.%s", mname);
|
|
} else if (strcmp(mname, "env") == 0 || strcmp(mname, "option_env") == 0) {
|
|
/* Compile-time env var read. Specifically: an
|
|
* `include!(concat!(env!("OUT_DIR"), …))` pattern
|
|
* indicates code generated by a build.rs that we
|
|
* cannot see. We surface the env! call so trace
|
|
* tools know to look for OUT_DIR. */
|
|
path = cbm_arena_sprintf(ctx->arena, "core.macros.%s", mname);
|
|
}
|
|
if (path) {
|
|
rust_emit_resolved_call(ctx, path, "lsp_macro", CBM_RUST_CONF_MACRO_KNOWN);
|
|
} else {
|
|
/* User-defined macro: try expanding via macro_rules!. */
|
|
rust_expand_user_macro(ctx, mname, node);
|
|
}
|
|
}
|
|
}
|
|
TSNode args = ts_node_child_by_field_name(node, "arguments", 9);
|
|
if (!ts_node_is_null(args))
|
|
rust_walk_macro_tokens(ctx, args);
|
|
/* Don't recurse normally below — we already drilled into args. */
|
|
return;
|
|
}
|
|
|
|
/* Push a fresh scope for blocks and constructs introducing new bindings. */
|
|
bool push_scope =
|
|
(strcmp(kind, "block") == 0 || strcmp(kind, "if_expression") == 0 ||
|
|
strcmp(kind, "if_let_expression") == 0 || strcmp(kind, "while_expression") == 0 ||
|
|
strcmp(kind, "while_let_expression") == 0 || strcmp(kind, "for_expression") == 0 ||
|
|
strcmp(kind, "match_arm") == 0 || strcmp(kind, "closure_expression") == 0);
|
|
|
|
CBMScope *saved = ctx->current_scope;
|
|
if (push_scope) {
|
|
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
|
|
}
|
|
|
|
/* if_let / while_let bind a pattern from the value's matched form.
|
|
* Modern tree-sitter-rust parses `if let X = y { ... }` as
|
|
* `if_expression` containing a `let_condition` child rather than as
|
|
* the legacy `if_let_expression`. Same for `while let`. We handle
|
|
* both shapes here. */
|
|
if (strcmp(kind, "if_let_expression") == 0 || strcmp(kind, "while_let_expression") == 0) {
|
|
TSNode pat = ts_node_child_by_field_name(node, "pattern", 7);
|
|
TSNode val = ts_node_child_by_field_name(node, "value", 5);
|
|
if (!ts_node_is_null(pat) && !ts_node_is_null(val)) {
|
|
const CBMType *vt = rust_eval_expr_type(ctx, val);
|
|
rust_bind_pattern(ctx, pat, vt);
|
|
}
|
|
}
|
|
if (strcmp(kind, "if_expression") == 0 || strcmp(kind, "while_expression") == 0) {
|
|
/* Look for a let_condition (or let_chain) anywhere in the
|
|
* condition slot. */
|
|
TSNode cond = ts_node_child_by_field_name(node, "condition", 9);
|
|
if (!ts_node_is_null(cond)) {
|
|
uint32_t nc2 = ts_node_named_child_count(cond);
|
|
/* Walk one level down for let_condition / let_chain. */
|
|
const char *ck = ts_node_type(cond);
|
|
TSNode targets[8];
|
|
int tcount = 0;
|
|
if (strcmp(ck, "let_condition") == 0) {
|
|
targets[tcount++] = cond;
|
|
} else if (strcmp(ck, "let_chain") == 0) {
|
|
for (uint32_t i = 0; i < nc2 && tcount < 8; i++) {
|
|
TSNode c2 = ts_node_named_child(cond, i);
|
|
if (strcmp(ts_node_type(c2), "let_condition") == 0) {
|
|
targets[tcount++] = c2;
|
|
}
|
|
}
|
|
}
|
|
for (int t = 0; t < tcount; t++) {
|
|
TSNode lc = targets[t];
|
|
TSNode pat = ts_node_child_by_field_name(lc, "pattern", 7);
|
|
TSNode val = ts_node_child_by_field_name(lc, "value", 5);
|
|
if (!ts_node_is_null(pat) && !ts_node_is_null(val)) {
|
|
const CBMType *vt = rust_eval_expr_type(ctx, val);
|
|
rust_bind_pattern(ctx, pat, vt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* for_expression: bind the loop variable from the iter's element type. */
|
|
if (strcmp(kind, "for_expression") == 0) {
|
|
TSNode pat = ts_node_child_by_field_name(node, "pattern", 7);
|
|
TSNode val = ts_node_child_by_field_name(node, "value", 5);
|
|
if (!ts_node_is_null(pat) && !ts_node_is_null(val)) {
|
|
const CBMType *vt = rust_eval_expr_type(ctx, val);
|
|
const CBMType *base = vt;
|
|
while (base && base->kind == CBM_TYPE_REFERENCE)
|
|
base = base->data.reference.elem;
|
|
const CBMType *elem = cbm_type_unknown();
|
|
if (base && base->kind == CBM_TYPE_SLICE) {
|
|
elem = base->data.slice.elem;
|
|
} else if (base && base->kind == CBM_TYPE_TEMPLATE) {
|
|
const char *nm = base->data.template_type.template_name;
|
|
if ((strstr(nm, "Vec") || strstr(nm, "VecDeque") || strstr(nm, "Iterator") ||
|
|
strstr(nm, "Range")) &&
|
|
base->data.template_type.arg_count > 0) {
|
|
elem = base->data.template_type.template_args[0];
|
|
}
|
|
if ((strstr(nm, "HashMap") || strstr(nm, "BTreeMap")) &&
|
|
base->data.template_type.arg_count > 1) {
|
|
/* Iter over (K, V) tuples. */
|
|
const CBMType *pair[2] = {base->data.template_type.template_args[0],
|
|
base->data.template_type.template_args[1]};
|
|
elem = cbm_type_tuple(ctx->arena, pair, 2);
|
|
}
|
|
}
|
|
rust_bind_pattern(ctx, pat, elem);
|
|
}
|
|
}
|
|
|
|
/* match_expression: per-arm scope handled when we descend. */
|
|
if (strcmp(kind, "match_arm") == 0) {
|
|
/* The match value type is captured by the parent walker — best
|
|
* effort: peek at the arm's pattern and let rust_bind_pattern do the
|
|
* work using cbm_type_unknown() if we cannot derive it. */
|
|
}
|
|
|
|
/* closure_expression: bind closure parameters.
|
|
*
|
|
* Priority order for each param:
|
|
* 1. Explicit type annotation (`|n: &i32|`) — use it directly.
|
|
* 2. `ctx->pending_closure_param_type` for the FIRST param when the
|
|
* surrounding call resolver inferred one (`.map(|x| ...)` on
|
|
* Iterator<T>).
|
|
* 3. Otherwise unknown.
|
|
*
|
|
* After binding, clear the pending type so it doesn't leak into a
|
|
* sibling closure. */
|
|
if (strcmp(kind, "closure_expression") == 0) {
|
|
const CBMType *hint = ctx->pending_closure_param_type;
|
|
ctx->pending_closure_param_type = NULL;
|
|
TSNode params = ts_node_child_by_field_name(node, "parameters", 10);
|
|
if (!ts_node_is_null(params)) {
|
|
uint32_t pc = ts_node_named_child_count(params);
|
|
for (uint32_t i = 0; i < pc; i++) {
|
|
TSNode p = ts_node_named_child(params, i);
|
|
/* For `parameter`-shaped nodes, peel off the type
|
|
* annotation if present; otherwise treat the whole node
|
|
* as the pattern. */
|
|
TSNode pat = p;
|
|
const CBMType *bound = (i == 0 && hint) ? hint : cbm_type_unknown();
|
|
if (strcmp(ts_node_type(p), "parameter") == 0) {
|
|
TSNode tn = ts_node_child_by_field_name(p, "type", 4);
|
|
TSNode pn = ts_node_child_by_field_name(p, "pattern", 7);
|
|
if (!ts_node_is_null(pn))
|
|
pat = pn;
|
|
if (!ts_node_is_null(tn)) {
|
|
bound = rust_parse_type_node(ctx, tn);
|
|
}
|
|
}
|
|
rust_bind_pattern(ctx, pat, bound);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Recurse. */
|
|
uint32_t nc = ts_node_child_count(node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (!ts_node_is_null(c)) {
|
|
rust_resolve_calls_in_node(ctx, c);
|
|
}
|
|
}
|
|
|
|
if (push_scope) {
|
|
ctx->current_scope = saved;
|
|
}
|
|
}
|
|
|
|
/* Process a single function: bind parameters / `self`, then walk body. */
|
|
static void rust_process_function(RustLSPContext *ctx, TSNode func_node, const char *parent_qn) {
|
|
TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4);
|
|
if (ts_node_is_null(name_node))
|
|
return;
|
|
char *name = rust_node_text(ctx, name_node);
|
|
if (!name || !name[0])
|
|
return;
|
|
|
|
const char *prefix = parent_qn ? parent_qn : ctx->module_qn;
|
|
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", prefix, name);
|
|
|
|
CBMScope *saved = ctx->current_scope;
|
|
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
|
|
|
|
/* Chalk-lite: capture the active function's type-parameter bounds
|
|
* and where-clause bounds into ctx so dispatch through generic
|
|
* receivers can route via the bound trait. */
|
|
int saved_bound_count = ctx->type_param_bound_count;
|
|
TSNode tp_list = ts_node_child_by_field_name(func_node, "type_parameters", 15);
|
|
if (!ts_node_is_null(tp_list)) {
|
|
char *tp_text = rust_node_text(ctx, tp_list);
|
|
if (tp_text)
|
|
rust_collect_bounds_from_text(ctx, tp_text);
|
|
}
|
|
TSNode where_clause = ts_node_child_by_field_name(func_node, "where_clause", 12);
|
|
if (!ts_node_is_null(where_clause)) {
|
|
char *wt = rust_node_text(ctx, where_clause);
|
|
if (wt)
|
|
rust_collect_bounds_from_text(ctx, wt);
|
|
}
|
|
|
|
/* Bind self for impl/trait methods. */
|
|
TSNode params = ts_node_child_by_field_name(func_node, "parameters", 10);
|
|
if (!ts_node_is_null(params)) {
|
|
uint32_t pc = ts_node_named_child_count(params);
|
|
for (uint32_t i = 0; i < pc; i++) {
|
|
TSNode p = ts_node_named_child(params, i);
|
|
const char *pk = ts_node_type(p);
|
|
if (strcmp(pk, "self_parameter") == 0) {
|
|
if (ctx->self_type_qn) {
|
|
/* Determine if &self / &mut self / self by reference. */
|
|
char *text = rust_node_text(ctx, p);
|
|
const CBMType *self_t = cbm_type_named(ctx->arena, ctx->self_type_qn);
|
|
if (text && strchr(text, '&')) {
|
|
self_t = cbm_type_reference(ctx->arena, self_t);
|
|
}
|
|
cbm_scope_bind(ctx->current_scope, "self", self_t);
|
|
}
|
|
continue;
|
|
}
|
|
if (strcmp(pk, "parameter") == 0) {
|
|
TSNode pat = ts_node_child_by_field_name(p, "pattern", 7);
|
|
TSNode tn = ts_node_child_by_field_name(p, "type", 4);
|
|
const CBMType *pt =
|
|
ts_node_is_null(tn) ? cbm_type_unknown() : rust_parse_type_node(ctx, tn);
|
|
if (!ts_node_is_null(pat))
|
|
rust_bind_pattern(ctx, pat, pt);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Walk function body. */
|
|
TSNode body = ts_node_child_by_field_name(func_node, "body", 4);
|
|
if (!ts_node_is_null(body)) {
|
|
rust_resolve_calls_in_node(ctx, body);
|
|
}
|
|
|
|
ctx->current_scope = saved;
|
|
ctx->enclosing_func_qn = NULL;
|
|
/* Restore bound-env count so the caller's bounds are unaffected. */
|
|
ctx->type_param_bound_count = saved_bound_count;
|
|
}
|
|
|
|
/* Walk an `impl_item`, processing each `function_item` inside its body
|
|
* with the appropriate `self_type_qn` (and `self_trait_qn` for trait impls). */
|
|
/* Chalk-lite: record a `T: Trait` bound in the per-function bound
|
|
* environment. The arrays grow by 8 to keep allocs cheap. */
|
|
static void rust_record_type_param_bound(RustLSPContext *ctx, const char *param_name,
|
|
const char *trait_qn) {
|
|
if (!ctx || !param_name || !trait_qn)
|
|
return;
|
|
/* Grow by 8s. */
|
|
if (ctx->type_param_bound_count % 8 == 0) {
|
|
int new_cap = ctx->type_param_bound_count + 8;
|
|
void *narr = cbm_arena_alloc(ctx->arena, new_cap * sizeof(*ctx->type_param_bounds));
|
|
if (!narr)
|
|
return;
|
|
if (ctx->type_param_bounds && ctx->type_param_bound_count > 0) {
|
|
memcpy(narr, ctx->type_param_bounds,
|
|
ctx->type_param_bound_count * sizeof(*ctx->type_param_bounds));
|
|
}
|
|
ctx->type_param_bounds = narr;
|
|
}
|
|
ctx->type_param_bounds[ctx->type_param_bound_count].param_name =
|
|
cbm_arena_strdup(ctx->arena, param_name);
|
|
ctx->type_param_bounds[ctx->type_param_bound_count].trait_qn =
|
|
cbm_arena_strdup(ctx->arena, trait_qn);
|
|
ctx->type_param_bound_count++;
|
|
}
|
|
|
|
/* Look up the first trait bound for a given type-param name. Returns
|
|
* NULL if `name` has no bound recorded. */
|
|
static const char *rust_lookup_type_param_bound(RustLSPContext *ctx, const char *name) {
|
|
if (!ctx || !name)
|
|
return NULL;
|
|
for (int i = 0; i < ctx->type_param_bound_count; i++) {
|
|
if (strcmp(ctx->type_param_bounds[i].param_name, name) == 0) {
|
|
return ctx->type_param_bounds[i].trait_qn;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse the impl/function's <T: Bound + Bound, U: Bound> + where clause
|
|
* text into the per-context bound environment. We don't reason about
|
|
* lifetimes; we record only trait bounds and associated-type bindings.
|
|
*
|
|
* Format we accept (simplified TOML-like grammar):
|
|
* `<T: Clone + Debug, U: Iterator<Item = V>>`
|
|
* `where T: Clone, U: Iterator<Item = V>`
|
|
*
|
|
* Multiple bounds are split on `+` (top-level), entries on `,`. */
|
|
static void rust_collect_bounds_from_text(RustLSPContext *ctx, const char *text) {
|
|
if (!ctx || !text)
|
|
return;
|
|
/* Walk text, find segments separated by `,` at top depth. For each
|
|
* segment, split on `:` to get (param, bound-list); split bounds on `+`
|
|
* at top depth. Resolve each bound through the path resolver to its QN. */
|
|
int len = (int)strlen(text);
|
|
int from = 0;
|
|
while (from < len) {
|
|
/* Skip whitespace + leading 'where'/punct. */
|
|
while (from < len && (text[from] == ' ' || text[from] == '\n' || text[from] == '<' ||
|
|
text[from] == ',' || text[from] == '>' || text[from] == 'w')) {
|
|
if (text[from] == 'w' && from + 5 < len && strncmp(text + from, "where", 5) == 0) {
|
|
from += 5;
|
|
} else {
|
|
from++;
|
|
}
|
|
}
|
|
if (from >= len)
|
|
break;
|
|
|
|
/* Param name. */
|
|
int name_start = from;
|
|
if (text[from] == '\'') {
|
|
/* Lifetime — skip. */
|
|
from++;
|
|
while (from < len && (isalnum((unsigned char)text[from]) || text[from] == '_'))
|
|
from++;
|
|
continue;
|
|
}
|
|
while (from < len && (isalnum((unsigned char)text[from]) || text[from] == '_'))
|
|
from++;
|
|
int name_end = from;
|
|
if (name_end == name_start) {
|
|
from++;
|
|
continue;
|
|
}
|
|
char *pname =
|
|
cbm_arena_strndup(ctx->arena, text + name_start, (size_t)(name_end - name_start));
|
|
/* Look for `:`. */
|
|
while (from < len && (text[from] == ' ' || text[from] == '\t'))
|
|
from++;
|
|
if (from >= len || text[from] != ':') {
|
|
/* No bound; skip to next entry. */
|
|
while (from < len && text[from] != ',' && text[from] != '>' && text[from] != '\n')
|
|
from++;
|
|
continue;
|
|
}
|
|
from++; /* consume `:` */
|
|
|
|
/* Bound list: split on `+` at depth 0, terminated by `,` / `>` /
|
|
* end of where clause. */
|
|
int depth = 0;
|
|
int bound_start = from;
|
|
while (from < len) {
|
|
char c = text[from];
|
|
if (c == '<' || c == '(' || c == '[')
|
|
depth++;
|
|
else if (c == '>' || c == ')' || c == ']') {
|
|
if (depth == 0)
|
|
break;
|
|
depth--;
|
|
} else if (depth == 0 && (c == '+' || c == ',' || c == '\n')) {
|
|
/* End of one bound. */
|
|
int b_end = from;
|
|
/* Trim trailing whitespace. */
|
|
while (b_end > bound_start && (text[b_end - 1] == ' ' || text[b_end - 1] == '\t')) {
|
|
b_end--;
|
|
}
|
|
/* Trim leading whitespace. */
|
|
int b_start = bound_start;
|
|
while (b_start < b_end && (text[b_start] == ' ' || text[b_start] == '\t')) {
|
|
b_start++;
|
|
}
|
|
if (b_end > b_start) {
|
|
char *btext =
|
|
cbm_arena_strndup(ctx->arena, text + b_start, (size_t)(b_end - b_start));
|
|
/* Strip any `<…>` associated-type suffix for the
|
|
* trait QN lookup — we keep the trait name only. */
|
|
char *langle = strchr(btext, '<');
|
|
if (langle)
|
|
*langle = '\0';
|
|
const char *qn = rust_resolve_path_expr(ctx, btext);
|
|
if (qn) {
|
|
rust_record_type_param_bound(ctx, pname, qn);
|
|
}
|
|
}
|
|
if (c == '+') {
|
|
from++;
|
|
bound_start = from;
|
|
continue;
|
|
}
|
|
/* End of entry. */
|
|
break;
|
|
}
|
|
from++;
|
|
}
|
|
/* Advance past entry terminator. */
|
|
if (from < len && (text[from] == ',' || text[from] == '\n'))
|
|
from++;
|
|
}
|
|
}
|
|
|
|
/* Helper: does `name` appear in the impl's `<T, U, ...>` type
|
|
* parameter list? Used to detect blanket impls (impl<T: Trait> X for T). */
|
|
static bool rust_impl_has_type_param(RustLSPContext *ctx, TSNode impl_node, const char *name) {
|
|
if (!name)
|
|
return false;
|
|
TSNode tp = ts_node_child_by_field_name(impl_node, "type_parameters", 15);
|
|
if (ts_node_is_null(tp))
|
|
return false;
|
|
uint32_t nc = ts_node_named_child_count(tp);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_named_child(tp, i);
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "constrained_type_parameter") == 0) {
|
|
char *nm = rust_node_text(ctx, c);
|
|
if (!nm)
|
|
continue;
|
|
/* For constrained_type_parameter, the name is the
|
|
* first identifier-y child. */
|
|
if (strcmp(ck, "constrained_type_parameter") == 0) {
|
|
TSNode lhs = ts_node_child_by_field_name(c, "left", 4);
|
|
if (!ts_node_is_null(lhs))
|
|
nm = rust_node_text(ctx, lhs);
|
|
}
|
|
if (nm && strcmp(nm, name) == 0)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void rust_process_impl(RustLSPContext *ctx, TSNode impl_node) {
|
|
TSNode type_node = ts_node_child_by_field_name(impl_node, "type", 4);
|
|
if (ts_node_is_null(type_node))
|
|
return;
|
|
char *type_text = rust_node_text(ctx, type_node);
|
|
if (!type_text)
|
|
return;
|
|
|
|
/* Detect blanket impl: `impl<T: Trait> ForeignTrait for T { ... }`
|
|
* where type_text is a name that appears in the impl's type
|
|
* parameters. In that case the receiver isn't a concrete type — it's
|
|
* any T satisfying the bound. We register the methods on the trait
|
|
* QN itself so dispatch through T: Trait finds them. */
|
|
TSNode trait_node = ts_node_child_by_field_name(impl_node, "trait", 5);
|
|
bool is_blanket =
|
|
!ts_node_is_null(trait_node) && rust_impl_has_type_param(ctx, impl_node, type_text);
|
|
const char *effective_recv = NULL;
|
|
|
|
if (is_blanket) {
|
|
char *tt = rust_node_text(ctx, trait_node);
|
|
if (tt)
|
|
effective_recv = rust_resolve_path_expr(ctx, tt);
|
|
} else {
|
|
effective_recv = rust_resolve_path_expr(ctx, type_text);
|
|
}
|
|
if (!effective_recv)
|
|
return;
|
|
|
|
const char *saved_self = ctx->self_type_qn;
|
|
const char *saved_trait = ctx->self_trait_qn;
|
|
ctx->self_type_qn = effective_recv;
|
|
ctx->self_trait_qn = NULL;
|
|
|
|
if (!ts_node_is_null(trait_node) && !is_blanket) {
|
|
char *tt = rust_node_text(ctx, trait_node);
|
|
if (tt)
|
|
ctx->self_trait_qn = rust_resolve_path_expr(ctx, tt);
|
|
}
|
|
|
|
TSNode body = ts_node_child_by_field_name(impl_node, "body", 4);
|
|
if (!ts_node_is_null(body)) {
|
|
uint32_t nc = ts_node_child_count(body);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(body, i);
|
|
if (ts_node_is_null(c) || !ts_node_is_named(c))
|
|
continue;
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "function_item") == 0) {
|
|
rust_process_function(ctx, c, effective_recv);
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx->self_type_qn = saved_self;
|
|
ctx->self_trait_qn = saved_trait;
|
|
}
|
|
|
|
void rust_lsp_process_file(RustLSPContext *ctx, TSNode root) {
|
|
if (ts_node_is_null(root))
|
|
return;
|
|
|
|
/* Pass 0: collect macro_rules! definitions before walking bodies so
|
|
* macro_invocation handlers can expand user-defined macros. */
|
|
rust_collect_macro_rules(ctx, root);
|
|
|
|
/* Record bare `mod foo;` declarations (file links). The pipeline
|
|
* uses these to know which sibling files to include in cross-file
|
|
* resolution. We just store them as Imports with a `mod:` prefix
|
|
* so the pipeline can distinguish them from `use` imports.
|
|
*
|
|
* `mod foo { ... }` (inline module) is NOT recorded — the body is
|
|
* already in this file. Only bare `mod foo;` declarations are. */
|
|
{
|
|
uint32_t rnc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < rnc; i++) {
|
|
TSNode c = ts_node_child(root, i);
|
|
if (ts_node_is_null(c))
|
|
continue;
|
|
if (strcmp(ts_node_type(c), "mod_item") != 0)
|
|
continue;
|
|
/* Inline mod has a `body` field; bare decl does not. */
|
|
TSNode body = ts_node_child_by_field_name(c, "body", 4);
|
|
if (!ts_node_is_null(body))
|
|
continue; /* inline */
|
|
TSNode mname = ts_node_child_by_field_name(c, "name", 4);
|
|
if (ts_node_is_null(mname))
|
|
continue;
|
|
char *name = rust_node_text(ctx, mname);
|
|
if (!name)
|
|
continue;
|
|
/* Surface as a synthetic CALLS edge from "<module>" to
|
|
* the sibling module so the cross-file pass picks it up
|
|
* via short-name fallback. We attribute it to the file's
|
|
* synthetic module-scope caller. */
|
|
const char *save_caller = ctx->enclosing_func_qn;
|
|
ctx->enclosing_func_qn = ctx->module_qn;
|
|
rust_emit_resolved_call(ctx,
|
|
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name),
|
|
"lsp_mod_decl", 0.70f);
|
|
ctx->enclosing_func_qn = save_caller;
|
|
}
|
|
}
|
|
|
|
/* Pass 1: bind module-level const/static so functions can see them. */
|
|
uint32_t nc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(root, i);
|
|
if (ts_node_is_null(c))
|
|
continue;
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "const_item") == 0 || strcmp(ck, "static_item") == 0) {
|
|
rust_process_statement(ctx, c);
|
|
}
|
|
}
|
|
|
|
/* Pass 2: walk every top-level item. */
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(root, i);
|
|
if (ts_node_is_null(c))
|
|
continue;
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "function_item") == 0) {
|
|
rust_process_function(ctx, c, NULL);
|
|
} else if (strcmp(ck, "impl_item") == 0) {
|
|
rust_process_impl(ctx, c);
|
|
} else if (strcmp(ck, "mod_item") == 0) {
|
|
/* Inline module — recurse into its declaration_list. */
|
|
TSNode body = ts_node_child_by_field_name(c, "body", 4);
|
|
if (!ts_node_is_null(body)) {
|
|
uint32_t mnc = ts_node_child_count(body);
|
|
for (uint32_t j = 0; j < mnc; j++) {
|
|
TSNode mc = ts_node_child(body, j);
|
|
if (ts_node_is_null(mc))
|
|
continue;
|
|
const char *mck = ts_node_type(mc);
|
|
if (strcmp(mck, "function_item") == 0) {
|
|
rust_process_function(ctx, mc, NULL);
|
|
} else if (strcmp(mck, "impl_item") == 0) {
|
|
rust_process_impl(ctx, mc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 11. Per-file entry: build registry + run
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Collect `use_declaration`s in the file and materialise our use map.
|
|
* Tree-sitter-rust models the pattern as:
|
|
*
|
|
* use_declaration → identifier | scoped_identifier | scoped_use_list |
|
|
* use_list | use_as_clause | use_wildcard.
|
|
*
|
|
* We expand each of these into one or more (alias, full-path) entries. */
|
|
static void rust_collect_uses(RustLSPContext *ctx, TSNode root) {
|
|
/* Recursive walker. */
|
|
typedef struct stack_t {
|
|
TSNode node;
|
|
struct stack_t *prev;
|
|
} stack_t;
|
|
stack_t *top = (stack_t *)cbm_arena_alloc(ctx->arena, sizeof(stack_t));
|
|
top->node = root;
|
|
top->prev = NULL;
|
|
while (top) {
|
|
TSNode n = top->node;
|
|
top = top->prev;
|
|
if (ts_node_is_null(n))
|
|
continue;
|
|
const char *k = ts_node_type(n);
|
|
if (strcmp(k, "use_declaration") == 0) {
|
|
char *full = rust_node_text(ctx, n);
|
|
if (full) {
|
|
if (strncmp(full, "use ", 4) == 0)
|
|
full += 4;
|
|
size_t len = strlen(full);
|
|
if (len > 0 && full[len - 1] == ';')
|
|
full[len - 1] = '\0';
|
|
/* Trim leading whitespace. */
|
|
while (*full == ' ')
|
|
full++;
|
|
/* Detect glob. */
|
|
size_t flen = strlen(full);
|
|
if (flen >= 3 && strcmp(full + flen - 3, "::*") == 0) {
|
|
char *mod = cbm_arena_strndup(ctx->arena, full, flen - 3);
|
|
rust_lsp_add_glob(ctx, convert_path_to_qn(ctx->arena, mod));
|
|
} else if (flen >= 1 && full[flen - 1] == '}') {
|
|
/* Brace list: prefix::{a, b as c, d}. */
|
|
char *lbr = strchr(full, '{');
|
|
if (lbr) {
|
|
size_t prefix_len = (size_t)(lbr - full);
|
|
/* Strip trailing "::" from prefix. */
|
|
while (prefix_len >= 2 && full[prefix_len - 1] == ':' &&
|
|
full[prefix_len - 2] == ':') {
|
|
prefix_len -= 2;
|
|
}
|
|
char *prefix = cbm_arena_strndup(ctx->arena, full, prefix_len);
|
|
char *body = cbm_arena_strdup(ctx->arena, lbr + 1);
|
|
size_t blen = strlen(body);
|
|
if (blen > 0 && body[blen - 1] == '}')
|
|
body[blen - 1] = '\0';
|
|
char *save = NULL;
|
|
char *tok = strtok_r(body, ",", &save);
|
|
while (tok) {
|
|
while (*tok == ' ')
|
|
tok++;
|
|
char *eb = tok + strlen(tok) - 1;
|
|
while (eb > tok && *eb == ' ')
|
|
*eb-- = '\0';
|
|
if (*tok == '\0') {
|
|
tok = strtok_r(NULL, ",", &save);
|
|
continue;
|
|
}
|
|
/* `Read` or `Read as R`. */
|
|
char *asp = strstr(tok, " as ");
|
|
char *alias = NULL;
|
|
char *path_part = tok;
|
|
if (asp) {
|
|
*asp = '\0';
|
|
alias = asp + 4;
|
|
while (*alias == ' ')
|
|
alias++;
|
|
} else {
|
|
alias = (char *)path_last_segment(tok);
|
|
}
|
|
char *full_path =
|
|
(strcmp(tok, "self") == 0)
|
|
? cbm_arena_strdup(ctx->arena, prefix)
|
|
: cbm_arena_sprintf(ctx->arena, "%s::%s", prefix, path_part);
|
|
rust_lsp_add_use(ctx, alias, full_path);
|
|
tok = strtok_r(NULL, ",", &save);
|
|
}
|
|
}
|
|
} else {
|
|
/* Single path; possibly followed by ` as X`. */
|
|
char *asp = strstr(full, " as ");
|
|
char *alias = NULL;
|
|
char *path_part = full;
|
|
if (asp) {
|
|
*asp = '\0';
|
|
alias = asp + 4;
|
|
while (*alias == ' ')
|
|
alias++;
|
|
} else {
|
|
alias = (char *)path_last_segment(full);
|
|
}
|
|
rust_lsp_add_use(ctx, alias, path_part);
|
|
}
|
|
}
|
|
}
|
|
/* Recurse into mod_item bodies so nested uses are captured too. */
|
|
if (strcmp(k, "mod_item") == 0 || strcmp(k, "source_file") == 0 ||
|
|
strcmp(k, "declaration_list") == 0) {
|
|
uint32_t nc = ts_node_child_count(n);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_child(n, i);
|
|
if (ts_node_is_null(c))
|
|
continue;
|
|
stack_t *nx = (stack_t *)cbm_arena_alloc(ctx->arena, sizeof(stack_t));
|
|
nx->node = c;
|
|
nx->prev = top;
|
|
top = nx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Build the registry from the per-file `result->defs`, `result->impl_traits`,
|
|
* and a Rust prelude seed. */
|
|
static void rust_build_registry_from_defs(CBMArena *arena, CBMTypeRegistry *reg,
|
|
CBMFileResult *result, const char *module_qn, TSNode root,
|
|
const char *source) {
|
|
|
|
cbm_registry_init(reg, arena);
|
|
cbm_rust_stdlib_register(reg, arena);
|
|
|
|
/* Phase A: register every Class/Type/Trait/Function/Method definition. */
|
|
for (int i = 0; i < result->defs.count; i++) {
|
|
CBMDefinition *d = &result->defs.items[i];
|
|
if (!d->qualified_name || !d->name)
|
|
continue;
|
|
|
|
// Every type-like container (Class/Struct/Type/Interface/Trait/Enum).
|
|
// Struct included so a Rust `struct Foo` (now labelled "Struct") registers
|
|
// as a type and its `impl Foo` methods/fields resolve.
|
|
if (cbm_label_is_type_like(d->label)) {
|
|
CBMRegisteredType rt;
|
|
memset(&rt, 0, sizeof(rt));
|
|
rt.qualified_name = d->qualified_name;
|
|
rt.short_name = d->name;
|
|
rt.is_interface =
|
|
(strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Trait") == 0);
|
|
cbm_registry_add_type(reg, rt);
|
|
}
|
|
|
|
if (d->label && (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0)) {
|
|
CBMRegisteredFunc rf;
|
|
memset(&rf, 0, sizeof(rf));
|
|
rf.qualified_name = d->qualified_name;
|
|
rf.short_name = d->name;
|
|
rf.min_params = -1;
|
|
|
|
/* Build FUNC sig from return_types / param_types. */
|
|
const CBMType **ret_types = NULL;
|
|
if (d->return_types) {
|
|
int count = 0;
|
|
while (d->return_types[count])
|
|
count++;
|
|
if (count > 0) {
|
|
ret_types = (const CBMType **)cbm_arena_alloc(
|
|
arena, (count + 1) * sizeof(const CBMType *));
|
|
for (int j = 0; j < count; j++) {
|
|
ret_types[j] =
|
|
rust_parse_return_type_text(arena, d->return_types[j], module_qn);
|
|
}
|
|
ret_types[count] = NULL;
|
|
}
|
|
} else if (d->return_type && d->return_type[0]) {
|
|
ret_types = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
|
|
ret_types[0] = rust_parse_return_type_text(arena, d->return_type, module_qn);
|
|
ret_types[1] = NULL;
|
|
}
|
|
const CBMType **param_types = NULL;
|
|
if (d->param_types) {
|
|
int count = 0;
|
|
while (d->param_types[count])
|
|
count++;
|
|
if (count > 0) {
|
|
param_types = (const CBMType **)cbm_arena_alloc(
|
|
arena, (count + 1) * sizeof(const CBMType *));
|
|
for (int j = 0; j < count; j++) {
|
|
param_types[j] =
|
|
rust_parse_return_type_text(arena, d->param_types[j], module_qn);
|
|
}
|
|
param_types[count] = NULL;
|
|
}
|
|
}
|
|
rf.signature = cbm_type_func(arena, d->param_names, param_types, ret_types);
|
|
|
|
if (strcmp(d->label, "Method") == 0 && d->parent_class) {
|
|
rf.receiver_type = d->parent_class;
|
|
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
|
|
CBMRegisteredType auto_t;
|
|
memset(&auto_t, 0, sizeof(auto_t));
|
|
auto_t.qualified_name = rf.receiver_type;
|
|
const char *dot = strrchr(rf.receiver_type, '.');
|
|
auto_t.short_name = dot ? cbm_arena_strdup(arena, dot + 1) : rf.receiver_type;
|
|
cbm_registry_add_type(reg, auto_t);
|
|
}
|
|
}
|
|
|
|
cbm_registry_add_func(reg, rf);
|
|
}
|
|
}
|
|
|
|
/* Phase B: walk the AST to extract struct fields + record `impl Trait
|
|
* for Type` linkage as embedded types. */
|
|
if (!ts_node_is_null(root)) {
|
|
uint32_t nc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode top = ts_node_child(root, i);
|
|
if (ts_node_is_null(top))
|
|
continue;
|
|
const char *tk = ts_node_type(top);
|
|
|
|
if (strcmp(tk, "struct_item") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(top, "name", 4);
|
|
TSNode body = ts_node_child_by_field_name(top, "body", 4);
|
|
if (ts_node_is_null(name_node) || ts_node_is_null(body))
|
|
continue;
|
|
char *tn = cbm_node_text(arena, name_node, source);
|
|
if (!tn || !tn[0])
|
|
continue;
|
|
const char *type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, tn);
|
|
|
|
/* Iterate field_declaration_list / ordered_field_declaration_list. */
|
|
if (strcmp(ts_node_type(body), "field_declaration_list") == 0) {
|
|
uint32_t fc = ts_node_named_child_count(body);
|
|
const char *fld_names[64];
|
|
const CBMType *fld_types[64];
|
|
int fld_count = 0;
|
|
for (uint32_t j = 0; j < fc && fld_count < 63; j++) {
|
|
TSNode fd = ts_node_named_child(body, j);
|
|
if (strcmp(ts_node_type(fd), "field_declaration") != 0)
|
|
continue;
|
|
TSNode fn = ts_node_child_by_field_name(fd, "name", 4);
|
|
TSNode ft = ts_node_child_by_field_name(fd, "type", 4);
|
|
char *fname = cbm_node_text(arena, fn, source);
|
|
if (!fname)
|
|
continue;
|
|
/* Build a temporary context for parsing types. */
|
|
RustLSPContext tmp;
|
|
memset(&tmp, 0, sizeof(tmp));
|
|
tmp.arena = arena;
|
|
tmp.source = source;
|
|
tmp.source_len = (int)strlen(source);
|
|
tmp.registry = reg;
|
|
tmp.module_qn = module_qn;
|
|
const CBMType *ft_t = rust_parse_type_node(&tmp, ft);
|
|
fld_names[fld_count] = fname;
|
|
fld_types[fld_count] = ft_t;
|
|
fld_count++;
|
|
}
|
|
if (fld_count > 0) {
|
|
for (int ti = 0; ti < reg->type_count; ti++) {
|
|
if (reg->types[ti].qualified_name &&
|
|
strcmp(reg->types[ti].qualified_name, type_qn) == 0) {
|
|
const char **names = (const char **)cbm_arena_alloc(
|
|
arena, (fld_count + 1) * sizeof(const char *));
|
|
const CBMType **types = (const CBMType **)cbm_arena_alloc(
|
|
arena, (fld_count + 1) * sizeof(const CBMType *));
|
|
for (int fi = 0; fi < fld_count; fi++) {
|
|
names[fi] = fld_names[fi];
|
|
types[fi] = fld_types[fi];
|
|
}
|
|
names[fld_count] = NULL;
|
|
types[fld_count] = NULL;
|
|
reg->types[ti].field_names = names;
|
|
reg->types[ti].field_types = types;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strcmp(tk, "trait_item") == 0) {
|
|
TSNode name_node = ts_node_child_by_field_name(top, "name", 4);
|
|
TSNode body = ts_node_child_by_field_name(top, "body", 4);
|
|
if (ts_node_is_null(name_node))
|
|
continue;
|
|
char *tn = cbm_node_text(arena, name_node, source);
|
|
if (!tn || !tn[0])
|
|
continue;
|
|
const char *trait_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, tn);
|
|
|
|
/* Mark as interface and collect method names. */
|
|
for (int ti = 0; ti < reg->type_count; ti++) {
|
|
if (!reg->types[ti].qualified_name)
|
|
continue;
|
|
if (strcmp(reg->types[ti].qualified_name, trait_qn) == 0) {
|
|
reg->types[ti].is_interface = true;
|
|
if (!ts_node_is_null(body)) {
|
|
const char *methods[64];
|
|
int mc = 0;
|
|
uint32_t bc = ts_node_named_child_count(body);
|
|
for (uint32_t j = 0; j < bc && mc < 63; j++) {
|
|
TSNode item = ts_node_named_child(body, j);
|
|
const char *ik = ts_node_type(item);
|
|
if (strcmp(ik, "function_item") != 0 &&
|
|
strcmp(ik, "function_signature_item") != 0)
|
|
continue;
|
|
TSNode mn = ts_node_child_by_field_name(item, "name", 4);
|
|
if (ts_node_is_null(mn))
|
|
continue;
|
|
char *mname = cbm_node_text(arena, mn, source);
|
|
if (mname)
|
|
methods[mc++] = mname;
|
|
}
|
|
if (mc > 0) {
|
|
const char **arr = (const char **)cbm_arena_alloc(
|
|
arena, (mc + 1) * sizeof(const char *));
|
|
for (int mi = 0; mi < mc; mi++)
|
|
arr[mi] = methods[mi];
|
|
arr[mc] = NULL;
|
|
reg->types[ti].method_names = arr;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Phase B1: walk top-level free `function_item`s to harvest their
|
|
* return types into the registry — `extract_defs` does not fill
|
|
* `return_type` for Rust free functions either, so a let-binding
|
|
* like `let v = pair();` would otherwise know nothing about pair's
|
|
* return tuple. */
|
|
if (!ts_node_is_null(root)) {
|
|
RustLSPContext tmp;
|
|
memset(&tmp, 0, sizeof(tmp));
|
|
tmp.arena = arena;
|
|
tmp.source = source;
|
|
tmp.source_len = (int)strlen(source);
|
|
tmp.registry = reg;
|
|
tmp.module_qn = module_qn;
|
|
|
|
uint32_t rnc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < rnc; i++) {
|
|
TSNode top = ts_node_child(root, i);
|
|
if (ts_node_is_null(top) || strcmp(ts_node_type(top), "function_item") != 0)
|
|
continue;
|
|
TSNode mn = ts_node_child_by_field_name(top, "name", 4);
|
|
TSNode rtn = ts_node_child_by_field_name(top, "return_type", 11);
|
|
if (ts_node_is_null(mn) || ts_node_is_null(rtn))
|
|
continue;
|
|
char *fname = cbm_node_text(arena, mn, source);
|
|
if (!fname)
|
|
continue;
|
|
const CBMType *ret = rust_parse_type_node(&tmp, rtn);
|
|
const char *fn_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, fname);
|
|
for (int k = 0; k < reg->func_count; k++) {
|
|
CBMRegisteredFunc *rf = ®->funcs[k];
|
|
if (!rf->qualified_name)
|
|
continue;
|
|
if (rf->receiver_type)
|
|
continue; /* free fns only */
|
|
if (strcmp(rf->qualified_name, fn_qn) != 0)
|
|
continue;
|
|
const CBMType **ret_arr =
|
|
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
|
|
ret_arr[0] = ret;
|
|
ret_arr[1] = NULL;
|
|
rf->signature = cbm_type_func(arena, NULL, NULL, ret_arr);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Phase A2: derive-macro synthesis.
|
|
*
|
|
* Real Rust code is saturated with `#[derive(Clone, Debug, …)]`.
|
|
* Without expanding proc-macros we can still synthesize the trait
|
|
* impl footprint that each well-known derive generates, so calls
|
|
* like `x.clone()` / `format!("{:?}", x)` / `MyT::default()` on the
|
|
* derived type actually resolve.
|
|
*
|
|
* We only synthesize the curated, high-frequency derives — anything
|
|
* unknown is left alone (per the FOLLOWUP doc's "no false edge"
|
|
* policy). Each synthesized impl:
|
|
* - registers a method (or static fn for `default`/`parse`) on
|
|
* the receiver type with the right short name and return type;
|
|
* - appends the trait's QN to the receiver's `embedded_types` so
|
|
* trait dispatch via `resolve_trait_method` walks it.
|
|
*/
|
|
{
|
|
/* Curated derive → (trait QN, [methods with sig sketch]) table. */
|
|
struct DeriveMethod {
|
|
const char *short_name;
|
|
const char *return_type; /* QN or NULL for unknown */
|
|
bool is_static; /* no `self` (e.g. `default`, `parse`) */
|
|
};
|
|
struct DeriveImpl {
|
|
const char *derive_name;
|
|
const char *trait_qn;
|
|
struct DeriveMethod methods[4]; /* NULL-terminated by empty short_name */
|
|
};
|
|
static const struct DeriveImpl derives[] = {
|
|
{"Clone", "core.clone.Clone", {{"clone", NULL, false}, {NULL, NULL, false}}},
|
|
{"Copy", "core.marker.Copy", {{NULL, NULL, false}}}, /* marker — no methods */
|
|
{"Debug", "core.fmt.Debug", {{"fmt", NULL, false}, {NULL, NULL, false}}},
|
|
{"Display", "core.fmt.Display", {{"fmt", NULL, false}, {NULL, NULL, false}}},
|
|
{"Default", "core.default.Default", {{"default", NULL, true}, {NULL, NULL, false}}},
|
|
{"PartialEq",
|
|
"core.cmp.PartialEq",
|
|
{{"eq", "bool", false}, {"ne", "bool", false}, {NULL, NULL, false}}},
|
|
{"Eq", "core.cmp.Eq", {{NULL, NULL, false}}}, /* marker only */
|
|
{"PartialOrd",
|
|
"core.cmp.PartialOrd",
|
|
{{"partial_cmp", NULL, false},
|
|
{"lt", "bool", false},
|
|
{"le", "bool", false},
|
|
{NULL, NULL, false}}},
|
|
{"Ord", "core.cmp.Ord", {{"cmp", NULL, false}, {NULL, NULL, false}}},
|
|
{"Hash", "core.hash.Hash", {{"hash", "()", false}, {NULL, NULL, false}}},
|
|
{"Send", "core.marker.Send", {{NULL, NULL, false}}},
|
|
{"Sync", "core.marker.Sync", {{NULL, NULL, false}}},
|
|
/* serde — extremely common. */
|
|
{"Serialize", "serde.Serialize", {{"serialize", NULL, false}, {NULL, NULL, false}}},
|
|
{"Deserialize",
|
|
"serde.Deserialize",
|
|
{{"deserialize", NULL, true}, {NULL, NULL, false}}},
|
|
/* clap derive — synthesizes the Parser interface. */
|
|
{"Parser",
|
|
"clap.Parser",
|
|
{{"parse", NULL, true},
|
|
{"try_parse", NULL, true},
|
|
{"parse_from", NULL, true},
|
|
{"try_parse_from", NULL, true}}},
|
|
{"Args", "clap.Args", {{NULL, NULL, false}}},
|
|
{"Subcommand", "clap.Subcommand", {{NULL, NULL, false}}},
|
|
{"ValueEnum", "clap.ValueEnum", {{NULL, NULL, false}}},
|
|
/* thiserror — adds the Error impl. */
|
|
{"Error", "core.error.Error", {{NULL, NULL, false}}},
|
|
};
|
|
const int derive_count = (int)(sizeof(derives) / sizeof(derives[0]));
|
|
|
|
for (int i = 0; i < result->defs.count; i++) {
|
|
CBMDefinition *d = &result->defs.items[i];
|
|
if (!d->qualified_name || !d->name)
|
|
continue;
|
|
/* `#[derive(...)]` rides on type-like defs — most often a struct or
|
|
* enum (now labelled "Struct"/"Enum"), also type aliases. Accept the
|
|
* whole type-like set so a derive on a struct is not dropped. */
|
|
if (!cbm_label_is_type_like(d->label))
|
|
continue;
|
|
if (!d->decorators)
|
|
continue;
|
|
|
|
/* Scan decorator strings for `#[derive(...)]`. */
|
|
for (int di = 0; d->decorators[di]; di++) {
|
|
const char *dec = d->decorators[di];
|
|
const char *p = strstr(dec, "derive");
|
|
if (!p)
|
|
continue;
|
|
const char *lparen = strchr(p, '(');
|
|
if (!lparen)
|
|
continue;
|
|
const char *rparen = strchr(lparen, ')');
|
|
if (!rparen)
|
|
continue;
|
|
/* Now walk between the parens, splitting on comma. */
|
|
const char *q = lparen + 1;
|
|
while (q < rparen) {
|
|
while (q < rparen && (*q == ' ' || *q == ','))
|
|
q++;
|
|
/* Find the end of the identifier (may be qualified
|
|
* like `serde::Serialize`). We grab the trailing
|
|
* segment as the derive name. */
|
|
const char *tok_start = q;
|
|
while (q < rparen && *q != ',' && *q != ' ')
|
|
q++;
|
|
if (q == tok_start)
|
|
break;
|
|
/* Trailing-segment after the last `::`. */
|
|
const char *short_start = tok_start;
|
|
for (const char *r = tok_start; r < q - 1; r++) {
|
|
if (r[0] == ':' && r[1] == ':')
|
|
short_start = r + 2;
|
|
}
|
|
size_t name_len = (size_t)(q - short_start);
|
|
if (name_len == 0 || name_len > 64)
|
|
continue;
|
|
/* Look up in curated table. */
|
|
for (int di2 = 0; di2 < derive_count; di2++) {
|
|
const struct DeriveImpl *di_entry = &derives[di2];
|
|
size_t entry_len = strlen(di_entry->derive_name);
|
|
if (entry_len != name_len)
|
|
continue;
|
|
if (strncmp(di_entry->derive_name, short_start, name_len) != 0)
|
|
continue;
|
|
|
|
/* Found a matching curated derive. Register the
|
|
* trait QN as an embedded_type on the receiver
|
|
* AND synthesize the method entries. */
|
|
CBMRegisteredType *rt = NULL;
|
|
for (int ti = 0; ti < reg->type_count; ti++) {
|
|
if (reg->types[ti].qualified_name &&
|
|
strcmp(reg->types[ti].qualified_name, d->qualified_name) == 0) {
|
|
rt = ®->types[ti];
|
|
break;
|
|
}
|
|
}
|
|
if (!rt)
|
|
break;
|
|
|
|
/* Append trait QN to embedded_types. */
|
|
int existing = 0;
|
|
if (rt->embedded_types) {
|
|
while (rt->embedded_types[existing])
|
|
existing++;
|
|
}
|
|
const char **new_arr = (const char **)cbm_arena_alloc(
|
|
arena, (existing + 2) * sizeof(const char *));
|
|
for (int k = 0; k < existing; k++) {
|
|
new_arr[k] = rt->embedded_types[k];
|
|
}
|
|
new_arr[existing] = di_entry->trait_qn;
|
|
new_arr[existing + 1] = NULL;
|
|
rt->embedded_types = new_arr;
|
|
|
|
/* Synthesize methods. Bound `mi < 4` BEFORE dereferencing
|
|
* methods[mi] so we never read methods[4] (OOB). */
|
|
for (int mi = 0; mi < 4 && di_entry->methods[mi].short_name; mi++) {
|
|
const struct DeriveMethod *dm = &di_entry->methods[mi];
|
|
CBMRegisteredFunc rf;
|
|
memset(&rf, 0, sizeof(rf));
|
|
rf.short_name = dm->short_name;
|
|
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", d->qualified_name,
|
|
dm->short_name);
|
|
/* Static methods (default/parse) have no
|
|
* receiver; method calls treat them as
|
|
* static path lookups via UFCS. */
|
|
rf.receiver_type = d->qualified_name;
|
|
rf.min_params = -1;
|
|
const CBMType *ret_t = cbm_type_unknown();
|
|
if (dm->return_type) {
|
|
if (strcmp(dm->return_type, "bool") == 0) {
|
|
ret_t = cbm_type_builtin(arena, "bool");
|
|
} else if (strcmp(dm->return_type, "()") == 0) {
|
|
ret_t = cbm_type_builtin(arena, "()");
|
|
}
|
|
} else if (dm->is_static) {
|
|
/* `default()`, `parse()` return Self. */
|
|
ret_t = cbm_type_named(arena, d->qualified_name);
|
|
}
|
|
const CBMType **ra = (const CBMType **)cbm_arena_alloc(
|
|
arena, 2 * sizeof(const CBMType *));
|
|
ra[0] = ret_t;
|
|
ra[1] = NULL;
|
|
rf.signature = cbm_type_func(arena, NULL, NULL, ra);
|
|
cbm_registry_add_func(reg, rf);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Phase B2: walk impl bodies to harvest each method's return type
|
|
* from the AST. The unified `extract_defs` extractor does not fill
|
|
* `return_type` for Rust impl methods, so without this pass our
|
|
* registered functions have no return-type signature and chained
|
|
* method calls (`File::open().read()`) break. */
|
|
if (!ts_node_is_null(root)) {
|
|
uint32_t rnc = ts_node_child_count(root);
|
|
for (uint32_t i = 0; i < rnc; i++) {
|
|
TSNode top = ts_node_child(root, i);
|
|
if (ts_node_is_null(top) || strcmp(ts_node_type(top), "impl_item") != 0)
|
|
continue;
|
|
TSNode type_node = ts_node_child_by_field_name(top, "type", 4);
|
|
TSNode body = ts_node_child_by_field_name(top, "body", 4);
|
|
if (ts_node_is_null(type_node) || ts_node_is_null(body))
|
|
continue;
|
|
char *type_name = cbm_node_text(arena, type_node, source);
|
|
if (!type_name || !type_name[0])
|
|
continue;
|
|
const char *type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
|
|
|
|
RustLSPContext tmp;
|
|
memset(&tmp, 0, sizeof(tmp));
|
|
tmp.arena = arena;
|
|
tmp.source = source;
|
|
tmp.source_len = (int)strlen(source);
|
|
tmp.registry = reg;
|
|
tmp.module_qn = module_qn;
|
|
tmp.self_type_qn = type_qn;
|
|
|
|
uint32_t bnc = ts_node_child_count(body);
|
|
for (uint32_t j = 0; j < bnc; j++) {
|
|
TSNode item = ts_node_child(body, j);
|
|
if (ts_node_is_null(item) || !ts_node_is_named(item))
|
|
continue;
|
|
if (strcmp(ts_node_type(item), "function_item") != 0)
|
|
continue;
|
|
TSNode mn = ts_node_child_by_field_name(item, "name", 4);
|
|
TSNode rtn = ts_node_child_by_field_name(item, "return_type", 11);
|
|
if (ts_node_is_null(mn) || ts_node_is_null(rtn))
|
|
continue;
|
|
char *mname = cbm_node_text(arena, mn, source);
|
|
if (!mname)
|
|
continue;
|
|
const CBMType *ret = rust_parse_type_node(&tmp, rtn);
|
|
/* Substitute Self -> receiver type so chains work. */
|
|
if (ret && ret->kind == CBM_TYPE_NAMED &&
|
|
strcmp(ret->data.named.qualified_name, "Self") == 0) {
|
|
ret = cbm_type_named(arena, type_qn);
|
|
}
|
|
/* Patch the registered function's signature. */
|
|
for (int k = 0; k < reg->func_count; k++) {
|
|
CBMRegisteredFunc *rf = ®->funcs[k];
|
|
if (!rf->receiver_type || !rf->short_name)
|
|
continue;
|
|
if (strcmp(rf->receiver_type, type_qn) != 0)
|
|
continue;
|
|
if (strcmp(rf->short_name, mname) != 0)
|
|
continue;
|
|
const CBMType **ret_arr =
|
|
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
|
|
ret_arr[0] = ret;
|
|
ret_arr[1] = NULL;
|
|
rf->signature = cbm_type_func(arena, NULL, NULL, ret_arr);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Phase C: encode `impl Trait for Type` as `embedded_types` on the
|
|
* receiver type so trait dispatch can find them. */
|
|
for (int i = 0; i < result->impl_traits.count; i++) {
|
|
CBMImplTrait *it = &result->impl_traits.items[i];
|
|
if (!it->struct_name || !it->trait_name)
|
|
continue;
|
|
const char *recv_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, it->struct_name);
|
|
const char *trait_qn = strstr(it->trait_name, "::")
|
|
? convert_path_to_qn(arena, it->trait_name)
|
|
: cbm_arena_sprintf(arena, "%s.%s", module_qn, it->trait_name);
|
|
|
|
CBMRegisteredType *rt = NULL;
|
|
for (int ti = 0; ti < reg->type_count; ti++) {
|
|
if (reg->types[ti].qualified_name &&
|
|
strcmp(reg->types[ti].qualified_name, recv_qn) == 0) {
|
|
rt = ®->types[ti];
|
|
break;
|
|
}
|
|
}
|
|
if (!rt) {
|
|
CBMRegisteredType auto_t;
|
|
memset(&auto_t, 0, sizeof(auto_t));
|
|
auto_t.qualified_name = recv_qn;
|
|
const char *dot = strrchr(recv_qn, '.');
|
|
auto_t.short_name = dot ? dot + 1 : recv_qn;
|
|
cbm_registry_add_type(reg, auto_t);
|
|
rt = ®->types[reg->type_count - 1];
|
|
}
|
|
/* Append trait_qn to embedded_types. */
|
|
int existing = 0;
|
|
if (rt->embedded_types) {
|
|
while (rt->embedded_types[existing])
|
|
existing++;
|
|
}
|
|
const char **new_arr =
|
|
(const char **)cbm_arena_alloc(arena, (existing + 2) * sizeof(const char *));
|
|
for (int j = 0; j < existing; j++)
|
|
new_arr[j] = rt->embedded_types[j];
|
|
new_arr[existing] = trait_qn;
|
|
new_arr[existing + 1] = NULL;
|
|
rt->embedded_types = new_arr;
|
|
}
|
|
}
|
|
|
|
void cbm_run_rust_lsp_with_manifest(CBMArena *arena, CBMFileResult *result, const char *source,
|
|
int source_len, TSNode root,
|
|
const struct CBMCargoManifest *manifest) {
|
|
if (!arena || !result || !source)
|
|
return;
|
|
const char *module_qn = result->module_qn ? result->module_qn : "rust";
|
|
|
|
CBMTypeRegistry reg;
|
|
rust_build_registry_from_defs(arena, ®, result, module_qn, root, source);
|
|
/* Finalize after all per-file adds so lookups during the walk
|
|
* use the hash buckets. */
|
|
cbm_registry_finalize(®);
|
|
|
|
RustLSPContext ctx;
|
|
rust_lsp_init(&ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls);
|
|
ctx.cargo_manifest = manifest;
|
|
/* Let the resolver inject synthetic syntactic calls for operator/macro
|
|
* desugaring so those recovered calls reach the CALLS-edge pipeline. */
|
|
ctx.syn_calls = &result->calls;
|
|
|
|
rust_collect_uses(&ctx, root);
|
|
/* Bridge any extracted CBMImports the unified extractor saw. */
|
|
for (int i = 0; i < result->imports.count; i++) {
|
|
CBMImport *imp = &result->imports.items[i];
|
|
if (imp->local_name && imp->module_path) {
|
|
rust_lsp_add_use(&ctx, imp->local_name, imp->module_path);
|
|
}
|
|
}
|
|
|
|
rust_lsp_process_file(&ctx, root);
|
|
|
|
/* Curated attribute proc-macro synthesis (Option B of the
|
|
* follow-up plan). Runs after the main walk so the synthetic
|
|
* edges are appended without affecting in-walk attribution. */
|
|
cbm_rust_synth_proc_macro_edges(arena, result);
|
|
}
|
|
|
|
void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
|
TSNode root) {
|
|
cbm_run_rust_lsp_with_manifest(arena, result, source, source_len, root, NULL);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════
|
|
* 12. Cross-file + batch
|
|
* ════════════════════════════════════════════════════════════════════ */
|
|
|
|
extern const TSLanguage *tree_sitter_rust(void);
|
|
|
|
/* Populate + finalize a Rust cross-file type registry from `defs`. Shared by the
|
|
* per-file resolver (cbm_run_rust_lsp_cross_with_manifest) and the build-once shared
|
|
* registry (cbm_rust_build_cross_registry) so both produce a byte-identical registry.
|
|
* `module_qn` is ONLY the fallback used to qualify a def's return type when that def
|
|
* carries no def_module_qn; pass NULL for the shared build (all_defs always carry
|
|
* def_module_qn — verified: 0 NULL across the C + Rust kernel corpora). */
|
|
static void rust_populate_cross_registry(CBMTypeRegistry *reg, CBMArena *arena,
|
|
CBMRustLSPDef *defs, int def_count,
|
|
const char *module_qn) {
|
|
cbm_registry_init(reg, arena);
|
|
cbm_rust_stdlib_register(reg, arena);
|
|
|
|
/* qn → (type index + 1), FIRST occurrence wins (mirrors the linear scans
|
|
* this map replaces). Both in-loop registry probes below — the receiver
|
|
* auto-registration check and the trait-linkage lookup — used to scan the
|
|
* UNFINALIZED registry linearly (no buckets exist before finalize): the
|
|
* checklist's lookup-in-registration-loop pattern. Invisible on small
|
|
* per-file builds; on the shared all_defs build (~1.4M entries) those
|
|
* scans were a constant ~63 s of the kernel run — and the sibling
|
|
* null-filter files waited on the build once-guard for exactly that long,
|
|
* which is why no resolution-side fix ever moved their wall time. Index,
|
|
* not pointer, because reg->types reallocs as it grows. */
|
|
CBMIdxMemo type_idx = {0};
|
|
for (int ti = 0; ti < reg->type_count; ti++) {
|
|
const char *qn = reg->types[ti].qualified_name;
|
|
if (qn) {
|
|
cbm_idxmemo_put_if_absent(&type_idx, arena, qn, ti);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < def_count; i++) {
|
|
CBMRustLSPDef *d = &defs[i];
|
|
if (!d->qualified_name || !d->short_name || !d->label)
|
|
continue;
|
|
const char *def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
|
|
|
|
// Every type-like container (Type/Class/Struct/Interface/Trait/Enum).
|
|
// Struct included so Rust structs (now labelled "Struct") register here.
|
|
if (cbm_label_is_type_like(d->label)) {
|
|
CBMRegisteredType rt;
|
|
memset(&rt, 0, sizeof(rt));
|
|
rt.qualified_name = cbm_arena_strdup(arena, d->qualified_name);
|
|
rt.short_name = cbm_arena_strdup(arena, d->short_name);
|
|
rt.is_interface = d->is_interface || strcmp(d->label, "Trait") == 0 ||
|
|
strcmp(d->label, "Interface") == 0;
|
|
cbm_registry_add_type(reg, rt);
|
|
cbm_idxmemo_put_if_absent(&type_idx, arena, rt.qualified_name, reg->type_count - 1);
|
|
}
|
|
|
|
if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) {
|
|
CBMRegisteredFunc rf;
|
|
memset(&rf, 0, sizeof(rf));
|
|
rf.qualified_name = cbm_arena_strdup(arena, d->qualified_name);
|
|
rf.short_name = cbm_arena_strdup(arena, d->short_name);
|
|
rf.min_params = -1;
|
|
|
|
/* Build sig from return_types text. */
|
|
const CBMType **ret_types = NULL;
|
|
if (d->return_types && d->return_types[0]) {
|
|
int count = 1;
|
|
for (const char *p = d->return_types; *p; p++) {
|
|
if (*p == '|')
|
|
count++;
|
|
}
|
|
ret_types =
|
|
(const CBMType **)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType *));
|
|
int idx = 0;
|
|
char *buf = cbm_arena_strdup(arena, d->return_types);
|
|
char *start = buf;
|
|
for (char *p = buf;; p++) {
|
|
if (*p == '|' || *p == '\0') {
|
|
char save = *p;
|
|
*p = '\0';
|
|
if (start[0]) {
|
|
ret_types[idx++] = rust_parse_return_type_text(arena, start, def_mod);
|
|
}
|
|
if (save == '\0')
|
|
break;
|
|
start = p + 1;
|
|
}
|
|
}
|
|
ret_types[idx] = NULL;
|
|
}
|
|
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
|
|
|
|
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
|
|
rf.receiver_type = cbm_arena_strdup(arena, d->receiver_type);
|
|
if (cbm_idxmemo_get(&type_idx, rf.receiver_type) < 0) {
|
|
CBMRegisteredType auto_t;
|
|
memset(&auto_t, 0, sizeof(auto_t));
|
|
auto_t.qualified_name = rf.receiver_type;
|
|
const char *dot = strrchr(d->receiver_type, '.');
|
|
auto_t.short_name = dot ? cbm_arena_strdup(arena, dot + 1) : rf.receiver_type;
|
|
cbm_registry_add_type(reg, auto_t);
|
|
cbm_idxmemo_put_if_absent(&type_idx, arena, auto_t.qualified_name,
|
|
reg->type_count - 1);
|
|
}
|
|
}
|
|
|
|
cbm_registry_add_func(reg, rf);
|
|
|
|
/* If trait_qn set: encode embedded_type linkage on receiver. */
|
|
if (rf.receiver_type && d->trait_qn && d->trait_qn[0]) {
|
|
CBMRegisteredType *rt = NULL;
|
|
int32_t tix = cbm_idxmemo_get(&type_idx, rf.receiver_type);
|
|
if (tix >= 0) {
|
|
rt = ®->types[tix];
|
|
}
|
|
if (rt) {
|
|
int existing = 0;
|
|
if (rt->embedded_types)
|
|
while (rt->embedded_types[existing])
|
|
existing++;
|
|
const char **new_arr = (const char **)cbm_arena_alloc(
|
|
arena, (existing + 2) * sizeof(const char *));
|
|
for (int j = 0; j < existing; j++)
|
|
new_arr[j] = rt->embedded_types[j];
|
|
new_arr[existing] = cbm_arena_strdup(arena, d->trait_qn);
|
|
new_arr[existing + 1] = NULL;
|
|
rt->embedded_types = new_arr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Finalise the cross-file registry now that all defs are added.
|
|
* (type_idx is arena-owned — freed with the registry's arena.) */
|
|
cbm_registry_finalize(reg);
|
|
}
|
|
|
|
/* Resolve one Rust file against an ALREADY-built (per-file or shared) registry. */
|
|
static void rust_resolve_against_registry(CBMArena *arena, const char *source, int source_len,
|
|
const char *module_qn, const CBMTypeRegistry *reg,
|
|
const char **import_names, const char **import_qns,
|
|
int import_count, TSNode root,
|
|
const struct CBMCargoManifest *manifest,
|
|
CBMResolvedCallArray *out, CBMFileResult *result) {
|
|
RustLSPContext ctx;
|
|
rust_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out);
|
|
ctx.cargo_manifest = manifest;
|
|
rust_collect_uses(&ctx, root);
|
|
for (int i = 0; i < import_count; i++) {
|
|
if (import_names[i] && import_qns[i]) {
|
|
rust_lsp_add_use(&ctx, import_names[i], import_qns[i]);
|
|
}
|
|
}
|
|
rust_lsp_process_file(&ctx, root);
|
|
if (result)
|
|
cbm_rust_synth_proc_macro_edges(arena, result);
|
|
}
|
|
|
|
/* Tier-2: build the Rust cross registry ONCE from all project defs, sealed
|
|
* read-only, and shared across every Rust file's resolve (mirrors C/py/cs/ts).
|
|
* Converts the pipeline's CBMLSPDef into CBMRustLSPDef inline (same field copy as
|
|
* pass_lsp_cross.c's pxc_lspdefs_to_rust, incl. trait_qn=NULL). module_qn=NULL is
|
|
* byte-identical because all_defs always carry def_module_qn. */
|
|
CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) {
|
|
if (!arena)
|
|
return NULL;
|
|
CBMTypeRegistry *reg = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*reg));
|
|
if (!reg)
|
|
return NULL;
|
|
CBMRustLSPDef *rdefs = NULL;
|
|
if (def_count > 0) {
|
|
rdefs = (CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
|
|
if (!rdefs)
|
|
return NULL;
|
|
for (int i = 0; i < def_count; i++) {
|
|
rdefs[i].qualified_name = defs[i].qualified_name;
|
|
rdefs[i].short_name = defs[i].short_name;
|
|
rdefs[i].label = defs[i].label;
|
|
rdefs[i].receiver_type = defs[i].receiver_type;
|
|
rdefs[i].def_module_qn = defs[i].def_module_qn;
|
|
rdefs[i].return_types = defs[i].return_types;
|
|
rdefs[i].embedded_types = defs[i].embedded_types;
|
|
rdefs[i].field_defs = defs[i].field_defs;
|
|
rdefs[i].method_names_str = defs[i].method_names_str;
|
|
rdefs[i].trait_qn = NULL;
|
|
rdefs[i].is_interface = defs[i].is_interface;
|
|
}
|
|
}
|
|
rust_populate_cross_registry(reg, arena, rdefs, def_count, /*module_qn=*/NULL);
|
|
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
|
|
return reg;
|
|
}
|
|
|
|
/* Cross-file Rust resolve using a pre-built shared registry (Tier-2). Skips the
|
|
* per-file registry build; just parse + resolve. Mirrors cbm_run_c_lsp_cross_with_registry. */
|
|
void cbm_run_rust_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
|
|
const char *module_qn, const CBMTypeRegistry *reg,
|
|
const char **import_names, const char **import_qns,
|
|
int import_count, TSTree *cached_tree,
|
|
const struct CBMCargoManifest *manifest,
|
|
CBMResolvedCallArray *out, CBMFileResult *result) {
|
|
if (!source || source_len <= 0 || !out || !reg)
|
|
return;
|
|
TSParser *parser = NULL;
|
|
TSTree *tree = cached_tree;
|
|
bool owns_tree = false;
|
|
if (!tree) {
|
|
parser = ts_parser_new();
|
|
if (!parser)
|
|
return;
|
|
ts_parser_set_language(parser, tree_sitter_rust());
|
|
tree = ts_parser_parse_string(parser, NULL, source, source_len);
|
|
owns_tree = true;
|
|
if (!tree) {
|
|
ts_parser_delete(parser);
|
|
return;
|
|
}
|
|
}
|
|
TSNode root = ts_tree_root_node(tree);
|
|
rust_resolve_against_registry(arena, source, source_len, module_qn, reg, import_names,
|
|
import_qns, import_count, root, manifest, out, result);
|
|
if (owns_tree) {
|
|
ts_tree_delete(tree);
|
|
if (parser)
|
|
ts_parser_delete(parser);
|
|
}
|
|
}
|
|
|
|
void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, int source_len,
|
|
const char *module_qn, CBMRustLSPDef *defs, int def_count,
|
|
const char **import_names, const char **import_qns,
|
|
int import_count, TSTree *cached_tree,
|
|
const struct CBMCargoManifest *manifest,
|
|
CBMResolvedCallArray *out) {
|
|
if (!source || source_len <= 0 || !out)
|
|
return;
|
|
|
|
TSParser *parser = NULL;
|
|
TSTree *tree = cached_tree;
|
|
bool owns_tree = false;
|
|
if (!tree) {
|
|
parser = ts_parser_new();
|
|
if (!parser)
|
|
return;
|
|
ts_parser_set_language(parser, tree_sitter_rust());
|
|
tree = ts_parser_parse_string(parser, NULL, source, source_len);
|
|
owns_tree = true;
|
|
if (!tree) {
|
|
ts_parser_delete(parser);
|
|
return;
|
|
}
|
|
}
|
|
TSNode root = ts_tree_root_node(tree);
|
|
|
|
/* Build registry from cross-file defs + stdlib (per-file). */
|
|
CBMTypeRegistry reg;
|
|
rust_populate_cross_registry(®, arena, defs, def_count, module_qn);
|
|
|
|
RustLSPContext ctx;
|
|
rust_lsp_init(&ctx, arena, source, source_len, ®, module_qn, out);
|
|
/* Workspace/dependency awareness for cross-CRATE path routing (#56).
|
|
* Mirrors the single-file path (cbm_run_rust_lsp_with_manifest). NULL
|
|
* when no Cargo.toml was parsed — in-crate resolution is unaffected. */
|
|
ctx.cargo_manifest = manifest;
|
|
rust_collect_uses(&ctx, root);
|
|
for (int i = 0; i < import_count; i++) {
|
|
if (import_names[i] && import_qns[i]) {
|
|
rust_lsp_add_use(&ctx, import_names[i], import_qns[i]);
|
|
}
|
|
}
|
|
rust_lsp_process_file(&ctx, root);
|
|
|
|
if (owns_tree) {
|
|
ts_tree_delete(tree);
|
|
if (parser)
|
|
ts_parser_delete(parser);
|
|
}
|
|
}
|
|
|
|
/* Manifest-free entry point. Preserves the pre-existing signature used by
|
|
* the unit tests (test_rust_lsp.c) and the batch wrapper — delegates to
|
|
* the manifest-aware variant with a NULL manifest. */
|
|
void cbm_run_rust_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
|
const char *module_qn, CBMRustLSPDef *defs, int def_count,
|
|
const char **import_names, const char **import_qns, int import_count,
|
|
TSTree *cached_tree, CBMResolvedCallArray *out) {
|
|
cbm_run_rust_lsp_cross_with_manifest(arena, source, source_len, module_qn, defs, def_count,
|
|
import_names, import_qns, import_count, cached_tree, NULL,
|
|
out);
|
|
}
|
|
|
|
void cbm_batch_rust_lsp_cross(CBMArena *arena, CBMBatchRustLSPFile *files, int file_count,
|
|
CBMResolvedCallArray *out) {
|
|
if (!files || file_count <= 0 || !out)
|
|
return;
|
|
|
|
for (int f = 0; f < file_count; f++) {
|
|
CBMBatchRustLSPFile *file = &files[f];
|
|
memset(&out[f], 0, sizeof(CBMResolvedCallArray));
|
|
if (!file->source || file->source_len <= 0)
|
|
continue;
|
|
|
|
CBMArena file_arena;
|
|
cbm_arena_init(&file_arena);
|
|
|
|
CBMResolvedCallArray file_out;
|
|
memset(&file_out, 0, sizeof(file_out));
|
|
|
|
cbm_run_rust_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
|
|
file->defs, file->def_count, file->import_names, file->import_qns,
|
|
file->import_count, file->cached_tree, &file_out);
|
|
|
|
if (file_out.count > 0) {
|
|
out[f].count = file_out.count;
|
|
out[f].items =
|
|
(CBMResolvedCall *)cbm_arena_alloc(arena, file_out.count * sizeof(CBMResolvedCall));
|
|
for (int j = 0; j < file_out.count; j++) {
|
|
CBMResolvedCall *src = &file_out.items[j];
|
|
CBMResolvedCall *dst = &out[f].items[j];
|
|
dst->caller_qn = src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
|
|
dst->callee_qn = src->callee_qn ? cbm_arena_strdup(arena, src->callee_qn) : NULL;
|
|
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
|
|
dst->confidence = src->confidence;
|
|
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
|
|
}
|
|
}
|
|
|
|
cbm_arena_destroy(&file_arena);
|
|
}
|
|
}
|