Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

353 lines
18 KiB
C++

/* rust_lsp.h — Type-aware call resolution for Rust source files.
*
* Architecture mirrors go_lsp / py_lsp: build a `CBMTypeRegistry` from the
* file's own definitions plus a small hand-rolled stdlib seed, then walk
* each function body with scope-tracked variable bindings, evaluating the
* type of each receiver expression and dispatching method calls through
* inherent impls, trait impls, deref/autoref, generics, and `Self`.
*
* The implementation is a structural mirror of the algorithm in
* `rust-analyzer` (`hir-def/resolver.rs` + `hir-ty/method_resolution.rs`)
* reverse-engineered into pure C with no new runtime dependencies. The
* goal is per-file (and cross-file) call attribution at >=90% parity with
* what rust-analyzer would produce for the same source — without the cost
* of a full IDE process.
*
* Public entry points:
* - `cbm_run_rust_lsp` — single-file resolution invoked from
* `cbm_extract_file()`.
* - `cbm_run_rust_lsp_cross` — cross-file resolution given a list of
* `CBMLSPDef`s gathered by the pipeline.
* - `cbm_batch_rust_lsp_cross` — batch wrapper that processes several
* files in one CGo call (per-file arenas + result copy).
*/
#ifndef CBM_LSP_RUST_LSP_H
#define CBM_LSP_RUST_LSP_H
#include "type_rep.h"
#include "scope.h"
#include "type_registry.h"
#include "lsp_neg_memo.h"
#include "../cbm.h"
#include "go_lsp.h" // for CBMLSPDef (pipeline def), used by the Tier-2 builder
/* Forward declaration — defined in rust_cargo.h. We keep it forward
* here to avoid pulling rust_cargo.h into every consumer that only
* needs the LSP API. */
struct CBMCargoManifest;
/* Global confidence assigned to LSP-resolved call edges. The pipeline's
* shared override resolver (`pipeline/lsp_resolve.h`) only admits entries
* scoring >= CBM_LSP_CONFIDENCE_FLOOR (0.6). Numbers here mirror the Go
* LSP so the call-edge mix from a polyglot project stays comparable. */
#define CBM_RUST_CONF_DIRECT 0.95f /* path::to::func or alias hit */
#define CBM_RUST_CONF_METHOD 0.95f /* inherent method dispatch */
#define CBM_RUST_CONF_TRAIT_SOLE 0.92f /* trait method, single impl */
#define CBM_RUST_CONF_TRAIT_AMB 0.85f /* trait method, many impls */
#define CBM_RUST_CONF_UFCS 0.93f /* T::method() / Self::new() */
#define CBM_RUST_CONF_PROMOTED 0.90f /* through Deref / blanket impl */
#define CBM_RUST_CONF_MACRO_KNOWN 0.85f /* known std macro mapped to fn */
#define CBM_RUST_CONF_OPERATOR 0.88f /* a+b → T::add (operator trait) */
/* Rust-flavoured LSP context: one per file, lifetime tied to a single
* `cbm_extract_file()` invocation (or the cross-file caller's arena). */
typedef struct {
CBMArena *arena;
const char *source;
int source_len;
const CBMTypeRegistry *registry;
CBMScope *current_scope;
/* Negative-lookup memo for the registry-pure resolve cascades (trait
* method / sole-trait-impl / free-func fallback). Active ONLY when the
* registry is sealed (read_only) — see lsp_neg_memo.h. Arena-backed,
* dies with the file. Zeroed by rust_lsp_init's memset (lazy alloc). */
CBMNegMemo neg_memo;
/* `use` map: parallel arrays mapping a local-name (the last segment, or
* the `as` alias) to its full module path (`std::collections::HashMap`,
* `crate::foo::Bar`). Glob imports go in `glob_module_qns` instead. */
const char **use_local_names;
const char **use_module_paths;
int use_count;
const char **glob_module_qns;
int glob_count;
/* Module-qualified name for this file (e.g. "<project>.<crate>.foo"). */
const char *module_qn;
/* Enclosing function context. `enclosing_func_qn` is the QN we attach
* to every emitted CBMResolvedCall as `caller_qn`. */
const char *enclosing_func_qn;
/* `Self` resolution: when inside `impl T { ... }` or
* `impl Trait for T { ... }`, `self_type_qn` is `T`'s QN. NULL outside
* an impl. `self_trait_qn` is the trait QN (only set for trait impls)
* — used when emitting trait method calls so we can prefer concrete
* implementations over the trait method when only one impl exists. */
const char *self_type_qn;
const char *self_trait_qn;
/* Closure parameter inference: when the call resolver descends into a
* call's argument list and the callee is a method like
* `.map(|x| ...)` / `.filter(|x| ...)`, it stashes the inferred type
* of the closure's first param here. The closure_expression handler
* consumes it (and clears it) when binding params, so a chain like
* `vec.iter().map(|x| x.method())` resolves `x.method()` correctly. */
const CBMType *pending_closure_param_type;
/* User-defined `macro_rules!` definitions collected during the
* pre-walk phase. Each rule stores its pattern + transcriber text;
* `macro_invocation` resolution attempts to match the invocation
* against each rule and then substitutes/re-walks the expansion.
*
* The arrays are doubling-grown out of `arena`. */
struct RustMacroRule **macro_rules_arr;
int macro_rules_count;
/* Recursion guard for macro expansion. Real macro_rules! can be
* recursive; we cap at 8 nested expansions to keep the walker
* bounded. */
int macro_expand_depth;
/* Pathological-input guard: counts the number of call-resolution
* + type-evaluation steps spent on the current file. When the
* counter exceeds the cap we stop attributing further calls so a
* malicious or hand-crafted file cannot wedge the resolver.
* Mirrors the cap added to `c_lsp.c` since the worktree branched
* (RUST_LSP_FOLLOWUP §B.2). */
int eval_step_count;
/* Cargo.toml manifest, when the caller has parsed one and routed
* it through. The resolver consults `dep_count`/`member_count` so
* paths beginning with a workspace member or declared dependency
* route to a sensible canonical form (`<crate>.<tail>`) instead of
* falling through to module-prefix fallback. NULL when no manifest
* is available — the resolver still works, just without workspace
* awareness. Owned by the caller; the RustLSPContext only borrows. */
const struct CBMCargoManifest *cargo_manifest;
/* Chalk-lite trait-bound environment for the *currently-active*
* function or impl. Populated at function/impl entry from the
* `<T: Bound + …>` parameter list and `where` clause. Consulted
* by trait method dispatch when the receiver is typed as a type
* parameter — we look up the param's bounds and try resolving
* the method on each trait. Also stores associated-type bindings
* (`T: Iterator<Item = U>` → `U` aliases the Item of `T`).
*
* Arrays are arena-allocated and reset on every function entry.
*/
struct {
const char *param_name; /* "T", "U", "Item" */
const char *trait_qn; /* "core.clone.Clone" */
} *type_param_bounds;
int type_param_bound_count;
struct {
const char *alias_name; /* "U" */
const char *aliased_to; /* "T.Item" (representational) */
} *type_param_aliases;
int type_param_alias_count;
/* Output: resolved (and unresolved-with-reason) calls accumulate here. */
CBMResolvedCallArray *resolved_calls;
/* Syntactic-call list (result->calls), borrowed from the per-file
* extraction result. The downstream pipeline only turns a resolved_call
* into a CALLS edge when a *syntactic* CBMCall with the same
* (enclosing_func_qn, callee short-name) exists here. Some calls the Rust
* resolver recovers — operator-trait desugaring (`a + b`) and method
* calls hidden inside macro token-trees (`format!("{}", d.label())`) —
* never appear in result->calls because the syntactic extractor cannot
* see them. When this is non-NULL the resolver injects matching synthetic
* CBMCall entries so those recovered calls become real edges. NULL in the
* cross-file path (no result available). */
CBMCallArray *syn_calls;
/* While >0, rust_emit_resolved_call also injects a matching synthetic
* CBMCall into `syn_calls` so the recovered call becomes an edge. Set
* around the macro-argument re-parse where the syntactic extractor never
* produced a call node. */
int inject_syn_calls;
/* CBM_LSP_DEBUG=1 in env enables verbose stderr trace. */
bool debug;
/* Per-ROOT-invocation macro-expansion memo. Kernel macro_rules are often
* self-recursive (define_sizes! re-invokes itself with @internal/@impls
* args) and the expansion fallback ("no pattern matched — still expand the
* first rule") makes that recursion non-convergent: with an unbounded
* BREADTH under the depth-8 guard, 2-44 source invocations exploded into
* ~200k expansions (each a full tree-sitter parse) ≈ 63 s per file.
* Within one expansion chain an identical (macro, substituted body) is
* walked once — identical text implies an identical walk, and recursive
* re-invocations have no distinct source site to attribute. Reset at each
* top-level invocation so distinct source sites keep their attribution. */
CBMNegMemo macro_memo;
} RustLSPContext;
/* Initialise an empty context for processing one file. */
void rust_lsp_init(RustLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *module_qn,
CBMResolvedCallArray *out);
/* Register a `use` alias. `local_name` may be the last segment of the path
* (`HashMap` for `use std::collections::HashMap`) or an `as` alias. The
* full `module_path` is stored verbatim (e.g. `std::collections::HashMap`).
* Glob imports (`use foo::*`) go through `rust_lsp_add_glob` instead. */
void rust_lsp_add_use(RustLSPContext *ctx, const char *local_name, const char *module_path);
void rust_lsp_add_glob(RustLSPContext *ctx, const char *module_qn);
/* Process every function/method in the file, walking statements and
* evaluating expression types as we go. */
void rust_lsp_process_file(RustLSPContext *ctx, TSNode root);
/* Evaluate the static type of a Rust expression node. Returns
* `cbm_type_unknown()` for anything we cannot type. */
const CBMType *rust_eval_expr_type(RustLSPContext *ctx, TSNode node);
/* Bidirectional variant: evaluate an expression with an `expected`
* hint available. The hint is used to:
* - disambiguate `Vec::new()` / `HashMap::default()` when the LHS
* of a let or function arg constrains the result type;
* - infer turbofish-free method generics (`s.parse()` typed as
* `Result<i32, _>`);
* - thread context into nested expressions (struct field init,
* match arm, if-else branches).
*
* `expected` may be NULL (no hint). If the synthesised type matches
* the expected, returns it; if a hint resolves an ambiguity that the
* synthesis path cannot, returns the hint-substituted form. */
const CBMType *rust_eval_expr_typed(RustLSPContext *ctx, TSNode node, const CBMType *expected);
/* Convert a `*type*` AST node (type_identifier, scoped_type_identifier,
* reference_type, primitive_type, …) into a CBMType. */
const CBMType *rust_parse_type_node(RustLSPContext *ctx, TSNode node);
/* Bind the bindings introduced by a let/for/match-pattern statement into
* the current scope. */
void rust_process_statement(RustLSPContext *ctx, TSNode node);
/* Look up an inherent method or field promoted through Deref / embedded
* trait blanket impls. Returns NULL if nothing matches. */
const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *type_qn,
const char *member_name);
/* Entry point — called from `cbm_extract_file()` after the unified
* extractor has filled `result->defs`, `result->imports`, and
* `result->impl_traits`. Builds a per-file registry, parses the `use`
* graph, and walks every function body emitting CBMResolvedCall entries. */
void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
/* Synthesize call edges for curated attribute proc-macros.
*
* For each `CBMDefinition` whose decorators include a known
* attribute proc-macro (`#[tokio::main]`, `#[tracing::instrument]`,
* `#[async_trait]`, …), emit synthetic edges from that definition
* to the helper functions the macro would have injected (e.g.
* `tokio::runtime::Runtime::new` + `block_on`). The edges carry
* strategy `lsp_proc_macro` so consumers can distinguish them from
* direct call attributions.
*
* Called automatically from `cbm_run_rust_lsp_with_manifest`. */
void cbm_rust_synth_proc_macro_edges(CBMArena *arena, CBMFileResult *result);
/* Same as `cbm_run_rust_lsp`, but accepts an optional Cargo manifest
* so the resolver can route paths whose head is a workspace member /
* declared dependency. Pass NULL to fall back to the manifest-free
* behaviour. */
void cbm_run_rust_lsp_with_manifest(CBMArena *arena, CBMFileResult *result, const char *source,
int source_len, TSNode root,
const struct CBMCargoManifest *manifest);
/* Register a curated subset of the Rust core/alloc/std prelude into the
* given registry. The seed is intentionally compact (~150 types, ~600
* methods) and covers Option/Result/Vec/String/HashMap/BTreeMap/Iterator
* plus the prelude trait method names (`clone`, `to_string`, `into`, …).
* Generated from `scripts/gen-rust-stdlib.py` if present, otherwise the
* hand-written `rust_stdlib_data.c` module is used. */
void cbm_rust_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
/* Register seeds for commonly-used external crates (serde, tokio,
* anyhow, clap, regex, log, futures, parking_lot, once_cell, chrono,
* uuid, reqwest, rayon). Best-effort curated entries — RUST_LSP_FOLLOWUP
* §A3. Calls into crates not in this seed remain `unresolved`. */
void cbm_rust_crates_register(CBMTypeRegistry *reg, CBMArena *arena);
/* --- Cross-file LSP resolution --- */
/* Cross-file definition record. The Rust LSP reuses the same shape as
* `CBMLSPDef` in `go_lsp.h` for compatibility with the pipeline plumbing
* — see that header for field semantics. We declare a separate typedef
* to allow Rust-specific fields without touching the Go ABI. */
typedef struct {
const char *qualified_name;
const char *short_name;
const char *label; /* "Function", "Method", "Type", "Trait" */
const char *receiver_type; /* for methods: receiver type QN (NULL for free fns) */
const char *def_module_qn; /* module QN where this def lives */
const char *return_types; /* "|"-separated return type texts */
const char *embedded_types; /* "|"-separated embedded type QNs */
const char *field_defs; /* "|"-separated "name:type" pairs */
const char *method_names_str; /* "|"-separated method names for traits */
const char *trait_qn; /* impl Trait for Type → set on Method defs */
bool is_interface; /* true for traits */
} CBMRustLSPDef;
/* Run cross-file resolution on a single file. */
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);
/* Same as `cbm_run_rust_lsp_cross`, plus an optional parsed Cargo manifest
* (NULL = manifest-free behaviour). The manifest lets call paths whose head
* is a workspace member / declared dependency route across the crate
* boundary (`crate_a::foo` → the def inside crate_a). Wired from the
* cross-file LSP pass (pass_lsp_cross.c) which builds the manifest once from
* the project root Cargo.toml. */
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);
/* Tier-2: build the project-wide Rust cross registry ONCE from all defs, finalize,
* and seal read-only. Shared across every Rust file's resolve (mirrors C/py/cs/ts).
* Def-driven → byte-identical entries to the per-file build. */
CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count);
/* Cross-file Rust resolve using a pre-built shared registry (Tier-2). Skips the
* per-file registry build; just parse + resolve. `manifest` = the same Cargo manifest
* the per-file path uses (cross-crate #56). `result` may be NULL (the cross path does
* not synthesise proc-macro edges). 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);
/* Per-file input for batch cross-file Rust LSP processing. */
typedef struct {
const char *source;
int source_len;
const char *module_qn;
TSTree *cached_tree;
CBMRustLSPDef *defs;
int def_count;
const char **import_names;
const char **import_qns;
int import_count;
} CBMBatchRustLSPFile;
/* Process several files in one CGo call (per-file arenas, result copy). */
void cbm_batch_rust_lsp_cross(CBMArena *arena, CBMBatchRustLSPFile *files, int file_count,
CBMResolvedCallArray *out);
#endif /* CBM_LSP_RUST_LSP_H */