mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
* feat(zig): static-gating analysis module + fixture (ported from feat/zig-static-gated-edges-v2) Squashes c6fe922c, 2f3c8e9e, fab088f4, 9b58af74, aef2ae83, 86b892ef, d5657861, f3780b3a: file-local comptime bool constants, cross-file flag resolution via the @import alias map, re-aliased const chains, == / != against known bools, else / else-if branch awareness. The module is self-contained; the hooks that call it land in the next commit. * feat(zig): stamp static-gated call sites through the scope pipeline Wires the ported gating module into the scope-resolution pipeline that now emits every Zig CALLS edge (PR #1432), replacing the parse-worker / call-processor hooks of the original branch, which targeted the legacy DAG path the merged provider no longer uses. Data flow, one new fact carried end to end: emitZigScopeCaptures stamps `@reference.static-gated` on a call capture whose anchor lies in a statically dead range (body of `if (CONST_FALSE)`, else of `if (CONST_TRUE)`), via the module's new `collectZigStaticGatedRanges` (line/col ranges, because a Capture keeps no node) scope-extractor marker -> `ReferenceSite.staticGated` buildReference -> `Reference.staticGated` references-to-edges, -> `GraphRelationship.staticGated` on the free-call-fallback, emitted CALLS edge (both emit paths, plus edges.ts (tryEmitEdge*) the generic bridge) local-backend impact -> `staticGated` on impact frontier edges Same marker idiom as Go's `@reference.callee-position` / `embedded-pointer`: zero-range, present or absent, so every ungated site's capture set is byte-identical and no other language changes. SCHEMA_BUMP 92 -> 93: parse-time captures changed. Cross-file constants (`if (cfg.FOO)` with `cfg = @import("cfg.zig")`) are NOT stamped yet: the module resolves them through `lookupBoolsForPath`, but the capture emitter runs per file in the parse worker with only `{ path, content }`, so it cannot see the sibling source. The two positive cross-file cases in zig-static-gating.test.ts are `it.skip` with that reason; the negative ones pass unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * feat(graph): add staticGated edge property Adds an optional `staticGated?: boolean` field to `GraphRelationship` that flags edges originating in code branches known at index time to be unreachable in production — e.g. `if (CONST_FALSE)` blocks where the condition reduces to a comptime-known `false`. Schema + persistence wiring: - `gitnexus-shared/src/graph/types.ts` — additive optional field on `GraphRelationship`; absent edges read identically to live ones. - `gitnexus/src/core/lbug/schema.ts` — `staticGated BOOLEAN` column on the `CodeRelation` REL table. - `gitnexus/src/core/lbug/csv-generator.ts` — appends a `staticGated` column (0/1) to the `relations.csv` written for bulk COPY ingest. - `gitnexus/src/core/lbug/lbug-adapter.ts` — fallback per-row `MATCH ... CREATE` insert reads the optional column and threads it into the relationship properties. No language has populated this field yet — the Zig hookup lands in the next commit. Existing DBs need a re-index for the new column to appear; existing readers are unchanged because the field is optional and absent on every other language's edges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit c8cd5efe27a77a5d1b3f05e3f4e89069b5c6b19e) * fix(zig): gate bare literal branches; AND the flag over deduplicated free-call sites PR #3161 review, two findings: 1. `stampZigStaticGating` returned early when the file declared no boolean constants, but `collectZigStaticGatedRanges` also folds bare literals, so `if (false) { foo(); }` in a constant-free file went unstamped. The early return is gone; the range walk runs for every file. 2. `emitFreeCallFallback` deduplicates CALLS edges per (caller, callee) and wrote `staticGated` from whichever site it met first, so a callee reached from one live site and one dead site was gated or not by traversal order. Emission is now deferred to the end of each file's sites and the flag is the AND over every site that collapsed into the edge: one live site keeps the edge live. The other emit path keys its dedup on the site range and was not affected; `collapseByCallerTarget` in the generic bridge would have the same shape but no language that sets the marker opts into it. Fixture + tests: `gated_bare_literal`, `live_and_gated_same_callee` (live site first) and `gated_then_live_same_callee` (dead site first) in zig-static-gating.test.ts. All 70 resolver suites (3,603 tests) pass with the shared emitter change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * test(zig): move the SCHEMA_BUMP pin to 93; rebaseline emit fingerprints for the staticGated column Three CI failures onf4954963, all consequences of this PR: - test/unit/incremental-parse-cache.test.ts pins SCHEMA_BUMP so concurrent bumps cannot collide; 92 -> 93 for #3161 (parse-time call captures gain `@reference.static-gated`), 92 added to the taken list. - bench/emit-persistence `measure.mjs --check` and `measure-streaming.mjs --check`: byte-identity fingerprints drift because every relationship row now ends in a `staticGated` cell. Regenerated with the inverse-operation evidence recorded under `_rebaselined_3161_static_gated_column` in both baseline files: stripping ONLY the new column from the emitted CSVs reproduces the prior fingerprints exactly (36 files, 3 rel_* files differ, 33 byte-identical; 36,000 PDG rows each +2 bytes), so no row moved between pair files or reordered. Timing and retention gates passed throughout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * docs(graph): state the CALLS contract on staticGated; say "provably unreachable at compile time" Review on #3161 (magyargergo): the flag must not redefine what a CALLS edge means. The field's doc now says so explicitly: CALLS still means "there is a resolved call site from A to B", never "B is reachable from A"; `staticGated` is additional, statically provable path-feasibility metadata, an opt-in analysis layer that no core pass acts on. The edge is emitted, persisted, traversed and counted exactly as before. Wording: "unreachable in production" -> "provably unreachable from the indexed source at compile time" on GraphRelationship, ReferenceSite and Reference. The index has no production build configuration and should not claim one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * feat(zig): surface staticGated on impact byDepth items; gate if-expressions and negated/parenthesized conditions Addresses the tri-review on #3161. - impact: the depth traversal already selected r.staticGated but dropped it when building the byDepth item. Forward it (present only when true) and document the field on the impact tool's byDepth contract. Traversal and ranking still do not act on it; that stays opt-in for consumers. - zig-static-gating: walk `if_expression` (`const x = if (c) a() else b();`) in addition to `if_statement`. The expression form has no field names and no else_clause wrapper, so the arms are located positionally (`ifExpressionArms`). Labeled-block arms are covered. - evalCond: `parenthesized_expression` is transparent, so `!(A and B)` and `((FLAG))` fold. Prefix `!` has no unary node in tree-sitter-zig; the header now says exactly which shapes fold instead of "simple negation". - fixture + tests: nine new cases (negation x2, parentheses x2, if-expression then/else/labeled-block x5). Cross-file `@import` constants remain skipped and now cite the tracking issue #3162. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * refactor(zig): drop the unreachable cross-file gating builders; document the real wiring gitnexus-check onb77cb4ed: `buildZigImportAliasMap` / `buildZigRawImportAliasMap` and the per-call ancestor walk (`isCallStaticGated`, `ifBranchDirection`, `nodesEqual`) had no caller anywhere. They were ported from the legacy call-processor design; the scope-resolution provider stamps ranges via `collectZigStaticGatedRanges` instead, and nothing populates the cross-file seam yet (#3162). Remove them so the module exports only what runs. The evaluator keeps `importAliases` + `lookupBoolsForPath` (the seam #3162 will fill); the header now says so explicitly and points at the actual wire-up (`stampZigStaticGating` in languages/zig/captures.ts) instead of the retired `configs/zig.ts` hook. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * refactor(zig): reuse descendantsOfType and tighten static-gated capture matching Walk if-nodes through tree-sitter instead of a hand-rolled stack, return the original capture array when nothing is gated, and register the marker as a known sub-tag so it cannot be mistaken for an anchor. Co-authored-by: Cursor <cursoragent@cursor.com> * style(zig): wrap a long else-clause assignment to satisfy prettier Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
12 KiB
TypeScript
248 lines
12 KiB
TypeScript
/**
|
|
* `ReferenceSite` — a pre-resolution usage fact collected by `ScopeExtractor`
|
|
* (RFC §3.2 Phase 1; Ring 2 PKG #919).
|
|
*
|
|
* One record per `@reference.*` capture. The extractor records:
|
|
* - the name being referenced (method/field/class name),
|
|
* - the source range,
|
|
* - the innermost lexical scope containing the reference,
|
|
* - the reference kind (call, read, write, inherits, etc.),
|
|
* - optional call-form classification from `provider.classifyCallForm`,
|
|
* - optional explicit-receiver hint for dotted calls (`user.save()`),
|
|
* - optional arity for call sites.
|
|
*
|
|
* Reference sites are consumed by the resolution phase (RFC §3.2 Phase 4)
|
|
* which routes each through `Registry.lookup` / `resolveTypeRef` and
|
|
* emits the final `Reference` record into `ReferenceIndex`.
|
|
*
|
|
* **Pre-resolution only.** `ReferenceSite` intentionally carries no
|
|
* `toDef`, `confidence`, or `evidence`. Those are populated by the
|
|
* resolution step that reads this record and produces a `Reference`
|
|
* (defined in `./types.ts`).
|
|
*/
|
|
|
|
import type { ParameterTypeClass } from './symbol-definition.js';
|
|
import type { Range, ScopeId } from './types.js';
|
|
|
|
/**
|
|
* What kind of usage this reference represents — the graph-edge kind
|
|
* emitted after resolution (`CALLS`, `READS`, `WRITES`, etc.).
|
|
*
|
|
* Matches the `kind` field on `Reference` in `./types.ts` so the
|
|
* resolution phase can pass it through without re-classification.
|
|
*/
|
|
export type ReferenceKind =
|
|
| 'call'
|
|
| 'read'
|
|
| 'write'
|
|
| 'type-reference'
|
|
| 'inherits'
|
|
| 'import-use'
|
|
// An identifier in object-literal property-value position
|
|
// (`{ emitScopeCaptures: emitCppScopeCaptures }`, shorthand `{ hook }`).
|
|
// Resolution is owned entirely by the post-finalize property-dispatch pass
|
|
// (`emitPropertyDispatchCalls` via the callable-gated finalized-bindings
|
|
// walker `findCallableBindingInScope`; `resolveReferenceSites` skips these
|
|
// sites), so a non-function value never produces a reference. Emitted as a `USES`
|
|
// reference edge — NOT `CALLS` (a registration is not an invocation;
|
|
// Kythe `ref` / Joern `METHOD_REF` precedent). The invocation side is
|
|
// recovered separately by the property-dispatch pass, which uses
|
|
// `propertyKey` to synthesize CALLS at member-call sites (#2437).
|
|
| 'value-ref'
|
|
// A macro invocation (`log!(...)` / `vec![...]`). Resolved against
|
|
// `Macro`-labeled definitions ONLY (see `MacroRegistry`) so a macro
|
|
// never aliases a same-named free function — macros and functions are
|
|
// disjoint namespaces. Emitted as a `USES` edge, not `CALLS`.
|
|
| 'macro';
|
|
|
|
/**
|
|
* How a call site binds its target. Informs `Registry.lookup` Step 2
|
|
* (type-binding path):
|
|
* - `'free'` — bare call (no receiver); resolution via lexical chain.
|
|
* - `'member'` — dotted call (`x.foo()`); resolution via receiver type.
|
|
* - `'constructor'` — `new Foo()`; receiver is the class itself.
|
|
* - `'index'` — index expression (`arr[0]`); rare as a dispatch site.
|
|
*
|
|
* Only meaningful for `kind === 'call'`; ignored for reads/writes.
|
|
*/
|
|
export type CallForm = 'free' | 'member' | 'constructor' | 'index';
|
|
|
|
export interface ReferenceSite {
|
|
/** The name being referenced (e.g., `'save'`, `'User'`, `'count'`). */
|
|
readonly name: string;
|
|
/**
|
|
* Optional raw, qualified form of the referenced name when the source wrote
|
|
* a qualified path (e.g. a C++ base `struct D : Other::Inner` yields
|
|
* `'Other::Inner'`). `name` keeps the simple tail (`'Inner'`) for the existing
|
|
* scope-chain contract; resolution normalizes this via `normalizeQualifiedName`
|
|
* and resolves it against the full-path `QualifiedNameIndex` BEFORE the
|
|
* simple-tail walk, so a same-tail nested base resolves to the correct
|
|
* sibling instead of the first-inserted one (issue #1982). Populated only by
|
|
* per-language captures that emit `@reference.qualified-name`; absent
|
|
* otherwise, in which case resolution is unchanged.
|
|
*/
|
|
readonly rawQualifiedName?: string;
|
|
/**
|
|
* Top-level generic/template arguments the source wrote ON this reference —
|
|
* `class UserValidator : IValidator<string>` yields `['string']` on the
|
|
* `inherits` site whose `name` is `IValidator`.
|
|
*
|
|
* `name` is the BASE name and stays that way: every lookup in resolution is
|
|
* keyed by it, and one declaration answers for every instantiation of itself.
|
|
* This records what the erasure threw away, so a consumer that needs the
|
|
* INSTANTIATION — receiver-bound interface dispatch, which must not fan a
|
|
* `IValidator<string>` receiver out to an `IValidator<int>` implementor
|
|
* (#2912) — can ask for it without re-parsing the source.
|
|
*
|
|
* Derived generically from the anchor capture's own text (see
|
|
* `collectReferenceSites`), so no language query change is needed: an emitter
|
|
* whose `@reference.inherits` anchor spans the whole base gets this for free,
|
|
* and one whose anchor is the bare name simply leaves it absent.
|
|
*
|
|
* ABSENT MEANS UNKNOWN, never "not generic" — the two are indistinguishable
|
|
* here, and only the first is safe to act on. Consumers must fail OPEN on
|
|
* absence (keep the target), matching `SymbolDefinition.typeParameters`.
|
|
*/
|
|
readonly typeArguments?: readonly string[];
|
|
/** Source-text range of this reference. */
|
|
readonly atRange: Range;
|
|
/**
|
|
* Innermost lexical scope that contains `atRange`. Resolved by the
|
|
* extractor via position lookup and frozen here so the resolution
|
|
* phase doesn't re-compute it per call.
|
|
*/
|
|
readonly inScope: ScopeId;
|
|
readonly kind: ReferenceKind;
|
|
/** Set when `kind === 'call'`. */
|
|
readonly callForm?: CallForm;
|
|
/**
|
|
* Explicit receiver for dotted calls (`user.save()` → `{ name: 'user' }`).
|
|
* Passed through to `Registry.lookup.explicitReceiver`.
|
|
*/
|
|
readonly explicitReceiver?: { readonly name: string };
|
|
/** Argument count at the call site; used by `provider.arityCompatibility`. */
|
|
readonly arity?: number;
|
|
/**
|
|
* Object-literal key under which a `value-ref` site registers its value
|
|
* (`{ emitScopeCaptures: emitHook }` → `'emitScopeCaptures'`; shorthand
|
|
* `{ emitHook }` → `'emitHook'`). Consumed by the property-dispatch pass
|
|
* to connect member-call sites (`x.emitScopeCaptures()`) to registered
|
|
* functions (#2437). Only set for `kind === 'value-ref'`.
|
|
*/
|
|
readonly propertyKey?: string;
|
|
/**
|
|
* Inferred argument types at the call site, one per argument. An
|
|
* empty-string entry means "unknown" — consumers narrowing overload
|
|
* candidates treat unknown as any-match. Populated by languages
|
|
* that can derive types from literals / constructor expressions
|
|
* (C#: `42` → `'int'`, `"alice"` → `'string'`).
|
|
*/
|
|
readonly argumentTypes?: readonly string[];
|
|
/**
|
|
* Optional per-argument type-shape sidecar for languages that need
|
|
* cv/ref/pointer distinctions during constraint filtering. This is
|
|
* intentionally separate from `argumentTypes`, which stays normalized
|
|
* for existing overload narrowing and conversion-rank logic.
|
|
*/
|
|
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
|
|
/**
|
|
* Compact encoding of a receiver that is itself an expression, so resolution
|
|
* can type it by folding over structure instead of re-parsing the receiver's
|
|
* source text.
|
|
*
|
|
* Format and the reason it is a string rather than `MixedChainStep[]` live in
|
|
* `receiver-chain-codec.ts` — briefly, the store's interning reviver re-shares
|
|
* objects only when they carry `nodeId` + `filePath`, which a chain step does
|
|
* not, so an object encoding would survive every warm load as fresh
|
|
* allocations.
|
|
*
|
|
* Absent whenever the receiver is a bare name, which is the overwhelming
|
|
* majority of sites — the field costs nothing where it is not needed.
|
|
*/
|
|
readonly receiverChain?: string;
|
|
/**
|
|
* This site sits in CALLEE position: it is the expression being invoked by an
|
|
* enclosing call, not a value the program otherwise consumes. Only ever set on
|
|
* `kind: 'read'` sites, and only by languages whose member-read capture also
|
|
* matches the callee of a member call (`obj.f()` yields both a `call` site on
|
|
* `f` and a `read` site on `obj.f`).
|
|
*
|
|
* It is a POSITION FACT, not a decision. Whether that read is redundant
|
|
* depends on what the tail resolves to, which the capture layer cannot know:
|
|
*
|
|
* - tail is a METHOD → the read duplicates the call's own edge and must be
|
|
* suppressed (an `ACCESSES → m` beside a `CALLS → m`
|
|
* at the same position is a phantom).
|
|
* - tail is a FIELD → the read is GENUINE. `h.dep.Work()` where
|
|
* `Work func() error` selects a func-typed field and
|
|
* then calls the value it holds; deleting the read
|
|
* erases the only evidence that the field was used
|
|
* (callback/hook structs, hand-rolled mocks).
|
|
*
|
|
* The suppression is therefore applied at edge emission, where the resolved
|
|
* target's kind is known — see `tryEmitEdge`. Absent on every site that is not
|
|
* in callee position, so nothing changes for languages that never set it.
|
|
*/
|
|
readonly inCalleePosition?: boolean;
|
|
/**
|
|
* This `inherits` site describes an embedded field written as a POINTER
|
|
* (`struct S { *T }`) rather than as a value (`struct S { T }`).
|
|
*
|
|
* Go's method-set rules make the two forms genuinely different, so the
|
|
* distinction cannot be normalized away without producing wrong answers
|
|
* (go.dev/ref/spec#Struct_types):
|
|
*
|
|
* - `S` embeds `T` → `MS(S)` and `MS(*S)` get promoted methods with
|
|
* receiver `T`; only `MS(*S)` also gets those with
|
|
* receiver `*T`.
|
|
* - `S` embeds `*T` → `MS(S)` AND `MS(*S)` get promoted methods with
|
|
* receiver `T` **or** `*T`.
|
|
*
|
|
* So with `func (t *T) Ping()`, `S{T}` does not implement a `Ping` interface
|
|
* by value while `S{*T}` does. Collapsing the forms makes both answers the
|
|
* same, and one of them is then wrong.
|
|
*
|
|
* A POSITION FACT, like `inCalleePosition`: the capture layer records how the
|
|
* field was spelled and resolution decides what it means. Set only by
|
|
* languages with pointer-embedding semantics (Go today); absent everywhere
|
|
* else, so every other language's sites stay byte-identical.
|
|
*/
|
|
readonly embeddedAsPointer?: boolean;
|
|
/**
|
|
* The call sits inside a branch that is provably unreachable from the
|
|
* indexed source at compile time — a Zig `if (CONST_FALSE)` body, or the
|
|
* `else` of `if (CONST_TRUE)`,
|
|
* where the condition folds to a comptime-known boolean. Set only when
|
|
* `kind === 'call'` and only by languages that compute static gating (Zig
|
|
* today); absent everywhere else, so every other site stays byte-identical.
|
|
* Threaded to `Reference.staticGated` and then `GraphRelationship.staticGated`.
|
|
*/
|
|
readonly staticGated?: boolean;
|
|
}
|
|
|
|
/**
|
|
* One step in a mixed receiver chain — the decoded form of a receiver that is
|
|
* itself an expression rather than a bare name.
|
|
*
|
|
* For `svc.getUser().address.save()`, the receiver of `save` decodes to
|
|
* `[{ kind: 'call', name: 'getUser' }, { kind: 'field', name: 'address' }]`
|
|
* over a base receiver of `svc`.
|
|
*
|
|
* Lives here rather than beside its producer because it is part of the
|
|
* ScopeExtractor output contract that this package owns: the producer
|
|
* (`extractMixedChain`) walks a tree-sitter AST and so must stay in the
|
|
* analyzer, but the shape it yields crosses into resolution.
|
|
*/
|
|
/**
|
|
* One hop in a receiver chain.
|
|
*
|
|
* `field` and `call` carry the member name they reach. `await` and `index` are
|
|
* NAME-FREE: the call step already holds the method name for an awaited call,
|
|
* and a subscript has no member name at all — an index expression's key is a
|
|
* value, not an identifier the resolver could look up. The codec encodes them
|
|
* as a bare sigil and rejects any trailing characters, so the encoder's
|
|
* non-empty-name guard stays live for exactly the two kinds it was written for.
|
|
*/
|
|
export type MixedChainStep =
|
|
| { kind: 'field' | 'call'; name: string }
|
|
| { kind: 'await' | 'index'; name?: undefined };
|