35 KiB
Rust-language kernel port (R7b) — the bug-for-bug checklist
("rust-lang" in the filename to avoid confusion with the kernel's own implementation language.)
Status: PORT COMPLETE (2026-07-20) — walker codegraph-kernel/src/rustlang.rs,
all gates below passed (parity sweeps 0-diff on ripgrep/tokio/rust-analyzer,
dump gates byte-identical ×3, DEFAULT_ROUTED += rust). This doc remains the
quirk reference for the walker. Survey basis: every TS-side branch a
.rs file exercises, with file:line anchors as of ce0ae30 (HEAD at survey
time). Every grammar-shape claim below was probed against the vendored
tree-sitter-rust v0.24.2 wasm (probe scripts in the session scratchpad), not
assumed. Read WITH docs/design/rust-kernel-migration-plan.md (§0a recipe, §5
gates) and docs/design/ccpp-kernel-port-checklist.md (format precedent).
Grammar prep is ALREADY STAGED (uncommitted at survey time): Cargo.toml
pins tree-sitter-rust = "=0.24.2", src/extraction/wasm/tree-sitter-rust.wasm
is vendored from tag 77a3747 (parser.c/scanner.c sha-matched against the
crates.io tarball), and rust is in VENDORED_WASM_LANGS (grammars.ts:291) —
replacing the 2023-era tree-sitter-wasms build (ABI 14 → 15). Per the recipe:
land the grammar bump FIRST and get the full suite green before the walker
exists. Probing showed the 0.24.2 shapes match the old build on every branch
below (function_modifiers nesting, token trees, impl fields, use shapes), so no
TS-side behavior change is expected from the bump — but the suite run is the
proof.
Architecture decisions
- No preParse.
rustExtractorhas nopreParsehook, so the route point'spreParsedSource(kernel/index.ts:76) is a no-op for rust — both arms parse raw bytes. Nothing to hoist, nothing to port. - Cargo repos take the DECODED path, not raw buffers.
rustResolver(resolution/frameworks/rust.ts:22,languages: ['rust'], detect =Cargo.tomlexists) has anextract()hook, and parse-worker.ts:93 forces any language with an applicable frameworkextract()onto the decodedextractFromSourcepath (framework nodes/refs merge into the decoded result). So on real Rust repos the kernel win is parse+walk+decode, never the buffers-to-store transport. Don't chase a raw-path number on ripgrep/tokio and conclude the port is broken. - The framework extractor itself needs NO port. It is regex-over-raw-source
TS (see §Frameworks below) and runs identically after either arm inside
extractFromSource(tree-sitter.ts:6736-6758). Only the tree-sitter-walk emissions below move to Rust. - One walker module (suggest
codegraph-kernel/src/rustlang.rs— "rust" alone collides with the crate language), registered inlangs.rs; per-filehas_error()→defer:like every walker. .rs→rustat detectLanguage (grammars.ts:78), no content sniffing, no dialect. MAX_FILE_SIZE (1 MiB, extraction/index.ts:132) and generated-file skips are orchestrator/TS-side and shared.
Extractor config (languages/rust.ts — 151 lines, read it whole)
Types: functionTypes=[function_item, function_signature_item] (the
latter = a trait method DECLARATION fn render(&self); — extracted so a
trait's method set is first-class); classTypes=[] (impl blocks instead);
methodTypes = same two; interfaceTypes=[trait_item] with
interfaceKind:'trait'; structTypes=[struct_item];
unionTypes=[union_item] (same body walk, distinct union node kind);
enumTypes=[enum_item];
enumMemberTypes=[enum_variant]; typeAliasTypes=[type_item];
importTypes=[use_declaration]; callTypes=[call_expression];
variableTypes=[let_declaration, const_item, static_item].
nameField=name, bodyField=body, paramsField=parameters,
returnField=return_type.
Hooks PRESENT (port each exactly):
- getReturnType = extractRustReturnType (rust.ts:14) — reads the
return_typefield; ifreference_type, unwrap to the first namedChild of typetype_identifier/scoped_type_identifier/generic_type(?? rt— falls back to the reference_type itself); then if type ∈ {primitive_type,unit_type,tuple_type} → undefined. Else:text.trim().replace(/<[^>]*>/g, ''), take last::segment, trim; must match/^[A-Za-z_]\w*$/else undefined;'Self'→ the marker'self'(resolved to the impl's own type at resolution time). QUIRKS: the non-greedy-ish/<[^>]*>/gstrip breaks on NESTED generics —Result<Vec<Foo>, E>→"Result, E>"→ regex fails → undefined (only single-level generics likeVec<Foo>→Vecsurvive).-> &Foounwraps toFoo;-> fmt::Result→Result. - getSignature (rust.ts:57) —
undefinedif noparametersfield; else raw text of params, plus' -> ' + <return_type raw text>when present. RawgetNodeText— multi-line params keep their newlines. - isAsync (rust.ts:67) — DEAD CODE BUG, PRESERVE: scans DIRECT children for
child.type === 'async'. Probed on v0.24.2:asyncnests inside afunction_modifierschild (pub async fnchildren:visibility_modifier, function_modifiers, fn, identifier, parameters, ->, <ret>, block), so isAsync always returns false — no rust node ever getsisAsync: true. The walker must reproduce false. - getVisibility (rust.ts:74) — direct child of type
visibility_modifier: text.includes('pub')→'public'else'private'; no modifier →'private'(sopub(crate)/pub(super)are all'public'). - getReceiverType (rust.ts:83) — walk PARENT chain to the nearest
impl_item; there: filter DIRECT namedChildren of typetype_identifier; if ≥1, return the LAST one's source text (source.substring(startIndex, endIndex)— UTF-16 units). If none, find the firstgeneric_typechild and return its innertype_identifiertext; else undefined. Never an impl parent → undefined. QUIRK/BUG, PRESERVE: forimpl Trait for Generic<T>the only direct type_identifier is the TRAIT (probe:impl Render for Container<T>→ typeIdents=[Render] → receiver =Render, the trait name — methods get qualifiedNameRender::renderand a contains edge from the trait node if one exists in-file).impl fmt::Display for Fieldsis fine (scoped_type_identifier isn't type_identifier → [Fields]).impl<T> Container<T>→ no direct type_identifiers → generic branch →Container. Note<T>type_parameters is its own child, its inner T is NOT a direct impl child. - extractImport (rust.ts:120) — signature = trimmed full
use …;text.useArg= FIRST namedChild of typescoped_use_list|scoped_identifier|use_list|identifier(a leadingvisibility_modifieronpub useis skipped by the find). moduleName =getRootModule(useArg): recurse intonamedChild(0)— if type ∈ {identifier, crate, super, self} return its text; ifscoped_identifierrecurse; else return the child's text; no child → whole node text. Souse crate::m::Item→ import node namedcrate;pub use self::sub::read→self;use foo;→foo. QUIRK:use std::fmt::*;parses asuse_wildcard, which is NOT in the useArg list → hook returns null → and because the hook exists, extractImport'sif (this.extractor.extractImport) return;(tree-sitter.ts:3350) fires → wildcard uses create NO import node and NO refs at all.handledRefsis not set → the generic path ALSO pushes oneimportsref for the root module name (crate/self/std/…) from the file node (tree-sitter.ts:3183-3194).
Hooks ABSENT (the walker must NOT do these): preParse, resolveName,
recoverMangledName, isMisparsedFunction, isConst, isStatic,
isExported, resolveBody, classifyClassNode, classifyMethodNode,
extractPropertyName, propertyTypes, fieldTypes, extraClassNodeTypes,
packageTypes/extractPackage, extractModifiers, synthesizeMembers,
extractBareCall, visitNode hook, skipBodilessClass, methodsAreTopLevel.
Consequences: every function/struct/enum/trait has isExported undefined
(file node false; extractVariable's ?? false → false); isStatic
undefined; no isConst means const_item/static_item extract as kind
'variable', never 'constant' (see extractVariable below).
tree-sitter.ts branches (anchors as of ce0ae30)
visitNode dispatch — what each top-level rust node hits
| Node | Branch | Behavior |
|---|---|---|
function_item (top level) |
functionTypes, tree-sitter.ts:994 → extractFunction:1517 | not inside class-like at file scope → extractFunction; first line of extractFunction (1522): if getReceiverType returns a value → extractMethod instead (this is how impl-block fns become methods — impl_item does NOT push a scope) |
function_signature_item |
same | in a trait body (trait pushed, class-like) → extractMethod; no body field → no body walk |
struct_item |
structTypes:1059 → extractStruct:1869 | body field required: unit structs struct Unit; have no body → NO node minted (1876, record_declaration exemption is C#-only). Tuple structs have body ordered_field_declaration_list → extracted. field_declaration children make NO nodes (rust has no fieldTypes) — visitNode recurses into them and finds nothing |
enum_item |
enumTypes:1064 → extractEnum:1914 | body enum_variant_list; enum_variant children → extractEnumMembers:1958 — name field path: one enum_member node from getChildByField(node,'name'), then return (variant payload bodies B(u32) / C { x } are never walked). Non-variant children (e.g. attribute_item) → visitNode (no-op) |
trait_item |
interfaceTypes:1054 → extractInterface:1834 | kind 'trait' (interfaceKind); extractInheritance sees the trait_bounds child (see below); body declaration_list children visited with the trait pushed → fn items become methods with QN Trait::name via nodeStack |
impl_item |
dedicated branch:1273-1276 → extractRustImplItem:5690 | emits the implements back-reference (below); skipChildren stays false → the declaration_list is then visited normally by the loop at 1295 (that's how impl members are reached; impl pushes NOTHING on the nodeStack) |
mod_item |
no branch | falls through → children visited. No module node, no qualifiedName prefix — items inside mod tests { } index as if at file scope. (frameworks/rust.ts:329 looks for kind === 'module' nodes and finds none from extraction — its nodes[0] fallback carries module resolution.) |
use_declaration |
importTypes:1209 → extractImport:3170 | import node + root-module ref (hook, above) + emitRustUseBindingRefs (3217-3219, rust-only, below) |
const_item / static_item (top level) |
variableTypes:1098 → extractVariable:2538 | generic fallback branch (2863-2881): kind = 'variable' ALWAYS (no isConst); iterate DIRECT namedChildren; every child of type identifier mints a node — for const MAX: u32 = OTHER; the children are identifier(MAX), primitive_type, identifier(OTHER) → TWO variable nodes, MAX and the phantom OTHER (probed). A non-identifier value (call, literal, array, struct_expression) → one node. Nodes get docstring + isExported:false, NO signature (unlike TS/Go branches). skipChildren=true, then scanFnRefSubtree (1110) capture-only. No instantiates/calls refs from top-level initializers — the value is never walked as a body |
let_declaration (top level) |
variableTypes | only legal inside bodies, so effectively never taken (bodies don't route through extractVariable); it's in variableTypes for the fn-ref dispatch + shadow prune. A body let is plain recursion inside visitFunctionBody |
type_item |
typeAliasTypes:1071 → extractTypeAlias:2890 | no resolveTypeAliasKind → plain type_alias node. QUIRK: the alias-value ref walk (2976) reads getChildByField(node,'value') — rust type_item's field is type, not value → null → a rust type alias emits NO reference to its aliased type |
associated const_item inside impl |
variableTypes | impl pushes nothing → !isInsideClassLikeNode() is true → extracted as a FILE-level variable node (contains edge from the file), e.g. impl Fields { const CAP … } → variable CAP. PRESERVE |
associated const_item inside trait body |
variableTypes gate FAILS | trait is pushed (class-like) and isClassScopeConstantAssignment needs node.type assignment → false → no node, but the else-ladder falls through with skipChildren=false → the const's value expression IS visited (a call in it emits a calls ref from the trait node) |
associated_type in trait, macro_definition, attribute_item, extern_crate_declaration |
no branch | recursed, nothing extracted |
macro_invocation (top level) |
no branch in visitNode | recursed into token_tree (raw tokens — nothing matches). Route macros are only extracted inside function bodies (visitFunctionBody:5141) — a top-level routes![…] emits nothing |
struct_expression |
INSTANTIATION_KINDS:359, visitNode:1255 + body walker:5145 | extractInstantiation (below). In practice struct_expressions live in bodies |
Node creation, IDs, qualified names
createNode(1308): id =generateNodeId(filePath, kind, name, startRow+1)=`${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`(tree-sitter-helpers.ts:18). The FILE node id is the literalfile:${filePath}(tree-sitter.ts:509), NOT hashed. Dedupe/self-checks compare ID STRINGS (same-(kind,name,line) collisions are routine —node_idsvec pattern in every walker).- endLine extension via resolveBody (1329) is a no-op for rust (no hook).
- contains edge from nodeStack top for every created node (1363).
- qualifiedName = nodeStack names joined
::(buildQualifiedName:1447; namespacePrefix is always empty outside C/C++). Methods with a receiver override it:composeReceiverQualifiedName(1435) =`${receiverType}::${name}`verbatim for rust (prefix empty → passes through, per the 1433 comment). - File node: kind
file, name basename, qualifiedName = filePath, endLine =source.split('\n').length, isExported false.
extractFunction / extractMethod for rust (1517 / 1737)
- extractFunction: receiverType present → extractMethod (1522). Name via
extractName→ nameFieldname(identifier). No misparse hook. Node gets docstring, signature, visibility, isExported:undefined, isAsync:false (bug above), isStatic:undefined, returnType. Then extractTypeAnnotations, extractDecoratorsFor (rustattribute_items are SIBLINGS, not children, and aren'tdecorator/annotation/marker_annotation/attributetypes → no decorates refs for rust, and the backward-sibling scan at 5013 stops at the first attribute_item anyway). Push node, walkbodyfield (block), pop. - extractMethod (reached for impl fns + trait members): receiverType computed
again (1742). Gate at 1747: not class-like AND no methodsAreTopLevel AND no
receiver → back to extractFunction (trait members pass via class-like; impl
fns via receiver). extraProps.qualifiedName =
Type::namewhen receiver (1790). Contains edge from the owner (1798-1813): only when receiver present AND not class-like — finds the FIRST node inthis.nodeswithname === receiverType && filePath === this.filePath && kind ∈ {struct,class,enum,trait}. Source-order dependent: an impl ABOVE its struct gets no contains edge.impl Trait for Generic<T>(receiver=trait bug) links to the TRAIT node if it's in-file. Then type annotations, decorators (no-op), body walk with the method pushed. - Nested
fninside an impl-method's body: visitFunctionBody:5245 → named → extractFunction → getReceiverType walks parents THROUGH the outer fn to the impl_item → receiver found → extractMethod → a nested helper indexes as a METHOD with QNType::inner+ contains edge from the type. PRESERVE. - structs/enums/traits declared inside a body are extracted there (5255-5275), contained by the enclosing function node.
extractCall (3684) — the rust paths
Generic else-branch (4312+), func = childForFieldName('function') ?? namedChild(0):
func.type === 'field_expression'(method callx.foo()): property =fieldfield (propertymisses). receiver = object/operand/argument fields → all null for rust →func.namedChild(0)(thevalue).- receiver type in LITERAL_RECEIVER_TYPES (373) → emit NOTHING (#1230).
Rust members of the set:
string_literal,raw_string_literal,integer_literal,float_literal,char_literal,boolean_literal. QUIRK: rustarray_expression/tuple_expression/struct_expressionreceivers are NOT in the set (it hasarray/array_literal, other grammars' names) —[1,2].len()falls through to the bare-name path and emitscallsreflen. PRESERVE. - receiver
identifier(not in SKIP_RECEIVERS {self,this,cls,super}) →recv.method. NOTE rustselfis node typeself, NOTidentifier, soself.own()skips this branch and lands on the fallthrough → bareown(same net effect as SKIP, different path — probed). - receiver
call_expression+ rust in the gate list (4413) → chained-call re-encode:innerFn = receiver.childForFieldName('function'),innerCallee = text(innerFn).replace(/->/g,'.').replace(/\s+/g,''); rust re-encodes ONLY wheninnerFn.type === 'scoped_identifier'(4455) →Foo::new().bar()→ refFoo::new().bar; an instance chainx.foo().bar()(innerFn field_expression) → barebar. When not re-encoding, calleeName = bare methodName. - receiver anything else (
field_expression2-hopv.field.method(),parenthesized_expression,await_expression,self) → bare methodName (probed all four).
- receiver type in LITERAL_RECEIVER_TYPES (373) → emit NOTHING (#1230).
Rust members of the set:
func.type === 'scoped_identifier'(4499) → calleeName = FULL text (Foo::new,m::helper2,std::mem::swap— whatever the source spells, whitespace included).- else → calleeName = raw func text: bare
helperfor identifier;generic_function(turbofishhelper::<T>) keeps the fullhelper::<T>text — unresolvable downstream, PRESERVE (probed).
Post-processing: the parenthesized-conversion regex (4530) can in principle
match (Foo)(x) shapes — rust parses a parenthesized callee as
parenthesized_expression so text starts ( → regex CAN fire; harmless and
must match. Template-arg strip (4542) and cpp fn-ptr fan-out (4556) are
c/cpp-gated — NOT for rust. Finally one calls ref {callerId, name, line =
call startRow+1, column = call startColumn (UTF-16)}. Inner calls of a chain
are ALSO visited (the body walker recurses after extractCall), so
Foo::new().bar() emits BOTH Foo::new().bar and Foo::new.
extractCall returns immediately when the nodeStack is empty — never the case
in practice (file node is pushed).
extractInstantiation — struct_expression (359, 4610)
ctor = constructor/type/name(rust)/namedChild(0). Not
composite_literal/instance_expression → generic path: text; strip from first
<; then lastDot = max(lastIndexOf('.'), lastIndexOf('::')) → keep trailing
segment (m::Widget { } → Widget); trim; emit instantiates ref at the
struct_expression's position. Fires from visitNode (top-level expressions) AND
visitFunctionBody (5145). Top-level const/static initializers never reach it
(extractVariable skips walking — quirk noted above).
Rocket route macros — extractRustRouteMacro (5048), body-walker-only (5141)
Gate: this.language === 'rust'; macroName = node.namedChild(0) (the
macro field identifier); name must be EXACTLY routes or catchers — a
scoped rocket::routes![…] has a scoped_identifier there whose text doesn't
match → skipped (PRESERVE). tokenTree = first namedChild of type token_tree.
fromId = nodeStack top. Walk tokenTree.child(i) (ALL children, anonymous
included): identifier tokens accumulate into parts (first one records
line/column); a , token flushes parts.join('::') as ONE ref
{referenceKind: references}; final flush after the loop (the closing ]
is not a flush trigger — the trailing path flushes at end). Probed token
stream: [ id :: id :: id , id ] — :: are anonymous and skipped by the
identifier/, switch. Consumed by resolveRustPathReference
(import-resolver.ts:1781).
emitRustUseBindingRefs (3451) — one imports ref per use binding
Called from extractImport for every use_declaration (3217). Recursive
collect(n, prefix) over the declaration's namedChildren:
identifier→ pushjoin(prefix, text)(join=prefix ? prefix+'::'+seg : seg)scoped_identifier→ pushprefix ? prefix+'::'+trim(fullText) : trim(fullText)(the FULL path text —crate::m::Item,self::sub::read)scoped_use_list→ prefix' = join(prefix, trim(text ofpathfield)); recurse intolistfield (?? namedChildren.find(type==='use_list'))use_list→ recurse each namedChild with same prefixuse_as_clause→ recurse thepathfield (?? namedChild(0)) — links the SOURCE path, not the alias (probed: fields are path/alias)- everything else (visibility_modifier,
use_wildcard, barecrate/self/supernodes) → ignored
Then per collected path: leaf = last :: segment; skip if leaf ∈
{self, super, crate, *} or empty; push {fromNodeId: file, referenceName: FULL
path, referenceKind:'imports', line/col of the collected node}. So
use crate::m::{A, B as C, sub::D} emits crate::m::A, crate::m::B,
crate::m::sub::D (plus the hook's root-module ref crate and the import
node named crate).
Inheritance — extractInheritance for rust (5291)
Only ONE child type matters for rust nodes: trait_bounds (5515, on
trait_item — supertraits trait Sub: Super + Display). Per bound child:
type_identifier→ name = textgeneric_type(Deserialize<'de>) → inner namedChild of typetype_identifier→ its texthigher_ranked_trait_bound(for<'de> Deserialize<'de>) → itsgeneric_typechild's inner type_identifier, else its own directtype_identifier- QUIRK, PRESERVE:
scoped_type_identifier(fmt::Debug) matches NO case → a path-qualified supertrait emits NOTHING (probed:trait Render: Base + fmt::Debug→ onlyBase).
Each yields an extends ref from the trait node at the bound's position.
Struct/enum extraction also calls extractInheritance; rust struct_item children
include field_declaration_list → the 5652 recursion descends, but rust
field_declaration always carries a field_identifier name so the Go
struct-embedding branch (5496) never fires. Verify with the torture fixture
anyway.
impl Trait for Type — extractRustImplItem (5690)
- hasFor = any child (ALL children) with
type === 'for' && !isNamed— plainimpl Type { }→ return (no edge; getReceiverType handles member attachment). - typeIdents = DIRECT namedChildren of type
type_identifier|generic_type|scoped_type_identifier; need ≥2 else return (v0.24.2 hastrait:andtype:FIELDS, but the code deliberately uses positional filtering — PRESERVE the positional logic). - traitNode = FIRST, typeNode = LAST. traitName: scoped_type_identifier →
source.substring(startIndex,endIndex)(fullfmt::Display); else getNodeText. typeName: generic_type → inner type_identifier text (Container) else text. - targetId =
findNodeByName(typeName)(5740): FIRST node inthis.nodeswith that name and kind ∈ {struct, enum, class} — NOT trait, and source-order dependent (the type must be defined EARLIER in the same file; cross-file impls emit nothing). If found: push {fromNodeId: the TYPE's node id (a back-reference), referenceName: traitName (full path text), referenceKind:'implements', line/col of the trait node}.
Type-annotation references (5752-6112)
rust ∈ TYPE_ANNOTATION_LANGUAGES (5753). For every function/method:
extractTypeAnnotations (5788) walks (a) the parameters field subtree and
(b) the return_type field subtree with extractTypeRefsFromSubtree (6090),
emitting one references ref per type_identifier leaf whose text isn't
in BUILTIN_TYPES (5768). The set includes the rust primitives (str bool i8…u128 usize isize f32 f64 char) — mostly redundant since rust primitives
parse as primitive_type, not type_identifier — plus cross-language rows
(error, String via the Scala block, Int/Any/…). Port the WHOLE set
verbatim: a rust type_identifier named String IS suppressed (Scala row),
while Vec/Option/Box/Self are NOT. QUIRKS, PRESERVE:
- Generic parameters are emitted:
fn get(&self) -> &T→ refT;Result<Baz, E>→ refsResult,Baz,E. -> Self→ refSelf(type_identifier, not builtin).scoped_type_identifier(fmt::Formatter) → only the innertype_identifierleafFormatter(thepathidentifier is not a type_identifier); the ref is UNQUALIFIED.whereclauses andtype_parametersbounds are NOT walked (params + return_type fields only; the type_parameters walk at 5863 is scala-gated).- The trailing
type_annotationchild lookup (5873, and extractVariableTypeAnnotation:6074 whose comment says "covers … Rust: Type") is a NO-OP for rust — the grammar has notype_annotationnode (let/const types are directtypefields). Dead comment, no behavior. - property_signature/method_signature branch (1283) — TS-only node types, never rust.
Static-member refs, cpp-isms — NOT rust
rust ∉ STATIC_MEMBER_LANGS (345) → extractStaticMemberRef no-ops (its call
in the body walker at 5218 must be a no-op in the walker too — cheap early
return). namespacePrefix, cppLocalFnPtrs, stack-construction, operator calls,
template strip: all c/cpp-gated, none apply.
Docstrings (tree-sitter-helpers.ts:95)
/// and //! are line_comment nodes; consecutive preceding named siblings
of the item accumulate (unshift → source order), then cleanCommentMarkers
strips ^\/\/[/!]?\s? per line (multiline gm — the CRLF ^-after-\r trap
from #1329 applies; use js_multiline_strip in docstring.rs). QUIRK,
PRESERVE: an attribute_item between the doc comment and the item breaks
the sibling chain — /// doc + #[derive(Debug)] + struct Doc → NO
docstring (probed; attribute_item is a named sibling and not a comment type).
DOCSTRING_WRAPPER_TYPES contains no rust wrappers → no climbing. Block
/** */-style (block_comment) is also accepted by the sibling scan and
/*-stripped.
Value-reference edges (398-931) — rust IS in VALUE_REF_LANGS (401)
Port the full machinery (crib go.rs/tsjs): CODEGRAPH_VALUE_REFS=0 kill;
MAX_VALUE_REF_NODES=20_000 caps BOTH the prune scan and each reader scan;
isGeneratedFile skip.
- Targets (captureValueRefScope:735): created nodes of kind
constant/variable (rust consts are
variable— still targets), name length ≥3 AND/[A-Z_]/test, parent scope id startingfile:(also class:/module:/struct:/enum: — rust consts always land under file:). Count per name in fileScopeValueCounts. - Reader scopes: every function/method/constant/variable node.
- Shadow prune (803-878): DFS of the whole tree counting declarators of
target names — rust cases:
const_item/static_item→ bumpchildForFieldName('name')(823-825);let_declaration(the shadow source, 827) → left ??pattern?? namedChild(0); if the pattern is anidentifierbump it, else bump every namedChild of the pattern (tuple patterns). bump() only countsidentifier/simple_identifiernodes whose text is a target. After the scan:declCount > fileScopeCount→ target deleted (a locallet MAX = …shadows the fileconst MAX). - Emission (880-930): per reader scope, DFS its node subtree (rust bodies are
children — the Dart/Pascal sibling pull at 891 is inert); each
identifier(also constant/name/simple_identifier — non-rust) whose text maps to a target and target ≠ self-id and name ≠ scope's own name and not yet seen → EDGE (not unresolved ref): {source: scopeId, target: targetId, kind:'references', metadata:{valueRef:true}}, deduped per (scope,target).
Function-as-value capture (#756) — RUST_SPEC (function-ref.ts:217)
idTypes={identifier}; dispatch:
arguments→args, assignment_expression→rhs(field right),
field_initializer→value(field value), array_expression→list,
static_item→varinit(field value), let_declaration→varinit(field value).
NO layers/unwrap/special/ungatedModes/addressOfOnly. QUIRK: const_item is
NOT in the dispatch — a const TABLE: [fn(); 2] = [a, b]; captures via the
inner array_expression, but const CB: fn() = handler; captures nothing
(static_item does). Capture mechanics (function-ref.ts:408-597):
- args/list: every namedChild is a candidate value.
- rhs: the
rightfield, with the param-storage skip — if the LHS's last identifier (/([A-Za-z_$][A-Za-z0-9_$]*)\s*$/on LHS text) EQUALS the RHS text, skip (o.cb = cb). - varinit: name/pattern field of type object_pattern/array_pattern/
tuple_pattern/struct_pattern → skip whole container (destructuring);
else the
valuefield. - normalizeValue: bare
identifier→ candidate (NAME_STOPLIST drops this/self/true/None/…). No unwrap →&handler(a rustreference_expression) yields NOTHING — rust captures only bare identifiers. explicitRef = false always (idTypes hit). - Capture fires from visitNode:990 AND visitFunctionBody:5137 AND scanFnRefSubtree (top-level initializers, halts at nested functionTypes, depth ≤12).
- Flush gate (flushFnRefCandidates:639): generated-file skip; candidate name
must be in definedHere (same-file function/method NAMES) ∪ importedNames.
QUIRK, PRESERVE: importedNames admits
SIMPLE_NAME(/^[A-Za-z_$][A-Za-z0-9_$]*$/) orQUALIFIED_IMPORTwith./\separators only — rust's::-separated import refs (crate::m::helper) match NEITHER, so rust use-imports contribute nothing to the gate except single-segment ones (use foo;→foo, and every root-module refcrate/self/std). Net: the rust fn-ref gate is effectively "defined in this file". Survivors dedupe on${fromNodeId}|${name}and push {referenceKind:'function_ref'}.
Misc shared paths
- Import/refs positions:
line = startPosition.row + 1,column = startPosition.column— UTF-16 code units (textutil::col16), as arestartIndex/endIndexsubstrings and.slice(0,100)truncations. - Refs carry NO filePath/language (the store denormalizes) — kernel wire contract is exactly extractFromSource's return.
extract()wraps everything: file node first, nodeStack=[fileId], no packageNode for rust; flushFnRefCandidates then flushValueRefs at the end.- Parse errors: the walker defers
has_error()files (defer:signal); wasm's error recovery is canonical. tree.delete()/source-release are wasm-side concerns.
Frameworks that consume rust extraction artifacts (stay TS-side)
rustResolver (resolution/frameworks/rust.ts) — detect: Cargo.toml.
extract()(regex over raw source, runs in extractFromSource AFTER either arm — NO port needed, but its INPUT contract must hold): emitsroutenodes with id`route:${filePath}:${line}:${METHOD}:${path}`(NOT hashed), kindroute, name`${METHOD} ${path}`, qualifiedName`${filePath}::route:${path}`, languagerust; plus onereferencesref per handler FROM the route node (these framework refs DO carry filePath+language — resolution/types' UnresolvedRef, unlike extraction refs). Covers#[get("/…")]attributes (Actix/Rocket), Axum.route("/p", get(h))chains, Actix builderweb::resource(...).to(h).- Extraction-side emissions the port MUST reproduce for rust resolution to
keep working: (a)
emitRustUseBindingRefs's FULL-pathimportsrefs and (b)extractRustRouteMacro's::-joinedreferencesrefs — both consumed byresolveRustPathReference(import-resolver.ts:1446/1781); (c) the root-moduleimportsref thatresolveModule/cargo-workspace mapping resolves (module refs likeuse foo;→src/foo.rs/ workspace crates). cargo-workspace.ts(path-aliases §) reads Cargo.toml manifests only — untouched.
Gates (per plan §5, no exceptions)
- Torture fixture
torture.rs(+ CRLF variant, derived in-memory), pinning at minimum: unit struct (NO node) / tuple struct / field struct; enum with unit+tuple+struct variants; trait with supertraits incl. a SCOPED one (fmt::Debug— dropped) +function_signature_item+ default method + associated type/const (no node; const value call attributes to trait); inherent impl (methods, associated const → file-levelvariable);impl Trait for Type;impl fmt::Display for Type(scoped trait name text);impl<T> Generic<T>(generic-branch receiver);impl Trait for Generic<T>(receiver = TRAIT bug); impl ABOVE its struct (no contains edge); nested fn inside an impl method (becomes a method);pub async fn(isAsync stays false);-> Self/-> &Foo/-> Vec<Foo>/-> Result<Vec<Foo>, E>(returnType undefined) /-> fmt::Result; turbofish call;Foo::new().bar()chain + instance chainx.foo().bar();self.method(); 2-hopv.field.method(); literal receiver"x".len()(nothing);m::helper()scoped call; struct_expression plain +m::Widget- inside fn args; use forms: single, grouped,
asalias, nested group path,pub use, wildcard (NO import node), bareuse foo;;const X: T = OTHER;(phantom second node) + static with array value; file-scope const read +let-shadowed const (value-ref prune); fn-ref shapes:register(handler),obj.cb = handler2,Widget { cb: handler },[cb_a, cb_b],static CB: fn() = handler,let cb = handler, tuple-let skip;routes![a::b::h1, h2]+catchers![x]+rocket::routes inside a body AND one at top level (skipped); doc comments incl.//!, a/* */block, and the attribute-breaks-docstring case; a mod with items (no module node, bare QNs).
- inside fn args; use forms: single, grouped,
- Parity sweeps (
scripts/kernel-parity.mjs, order-sensitive full-object): ripgrep (small), tokio (medium), rust-analyzer (large) — all three also exercise heavypub usere-export hubs and macro use. Then full-init dump-diffs byte-identical (kernel arm vsCODEGRAPH_KERNEL=0,dump-graph.mjs, cmp) on the same three. - Deferral-rate guard: default
--max-deferral 0.1and expect FAR under it — rust is not macro-mangled C; parse-error incidence should sit in the ts/java/py/go norm (0–0.42%). Double-digit deferral on a rust sweep means a broken walker, not grammar reality (the c/cpp 0.5 exemption does NOT carry over). - Grammar-bump isolation: the vendored v0.24.2 wasm +
=0.24.2crate pin land FIRST with the full suite green (kernel-grammar-parity sha-matches parser.c; crate + wasm move together or it fails). - Suite green with
CODEGRAPH_KERNEL_EXPECT=1; unit tests for the walker in__tests__/kernel-rustlang-parity.test.ts(or folded into the existing parity suites); changelog rides the existing kernel entry. DEFAULT_ROUTED += rust(kernel/index.ts:37) only after ALL of the above.- Post-route perf sanity: remember decision §arch-2 — Cargo repos take the decoded path (framework extract()), so measure the parse-loop, not the raw-buffer transport.