From f495496383a217b40151af5ef97caf53da6a17c7 Mon Sep 17 00:00:00 2001 From: Garrett Griffin-Morales Date: Thu, 3 Sep 2026 10:12:31 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG --- .../core/ingestion/languages/zig/captures.ts | 3 +- .../passes/free-call-fallback.ts | 46 +++++++++++++------ .../zig-static-gating/src/main.zig | 35 ++++++++++++++ .../resolvers/zig-static-gating.test.ts | 12 +++++ 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/zig/captures.ts b/gitnexus/src/core/ingestion/languages/zig/captures.ts index 2918394b4..001bf798d 100644 --- a/gitnexus/src/core/ingestion/languages/zig/captures.ts +++ b/gitnexus/src/core/ingestion/languages/zig/captures.ts @@ -34,8 +34,9 @@ const NO_IMPORT_ALIASES: ZigImportAliasMap = new Map(); * cross-file alias walk in `zig-static-gating.ts` needs the repo file list, * which the capture layer does not see. */ function stampZigStaticGating(out: readonly CaptureMatch[], root: SyntaxNode): CaptureMatch[] { + // No early return on an empty constant table: `collectZigStaticGatedRanges` + // also folds bare literals (`if (false) { ... }`), which need no constants. const bools = buildZigBoolConstMap(root); - if (bools.size === 0) return [...out]; const ranges = collectZigStaticGatedRanges(root, bools, NO_IMPORT_ALIASES, () => undefined); if (ranges.length === 0) return [...out]; return out.map((m) => { diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 8cf73cbb0..0457767b2 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -183,6 +183,8 @@ export function emitFreeCallFallback( }; for (const parsed of parsedFiles) { + type PendingRel = { rel: Parameters[0]; gatedAll: boolean }; + const pending = new Map(); const bindingCandidatesByScope = options.freeCallsRequireInstanceOwnership === true ? new Map>() @@ -616,26 +618,40 @@ export function emitFreeCallFallback( tgtGraphId, ); const relId = `rel:CALLS:${callerGraphId}->${tgtGraphId}`; + // One edge per (caller, callee): `staticGated` is the AND over every site + // that collapses into it, so a callee reached from one live site and one + // dead site stays live whichever site the walk meets first. Emission is + // deferred to the end of this file's sites for that reason. + const pendingRel = pending.get(relId); + if (pendingRel !== undefined) { + if (site.staticGated !== true) pendingRel.gatedAll = false; + continue; + } if (seen.has(relId)) continue; seen.add(relId); - graph.addRelationship({ - id: relId, - sourceId: callerGraphId, - targetId: tgtGraphId, - type: 'CALLS', - confidence: 0.85, - // Match legacy DAG's reason convention so consumers that - // assert `reason === 'import-resolved'` keep working. The - // construction-site marker is opt-in for the same reason. - reason: constructionSiteReason( - fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call', - site, - options.markConstructionSites, - ), - ...(site.staticGated === true ? { staticGated: true } : {}), + pending.set(relId, { + gatedAll: site.staticGated === true, + rel: { + id: relId, + sourceId: callerGraphId, + targetId: tgtGraphId, + type: 'CALLS', + confidence: 0.85, + // Match legacy DAG's reason convention so consumers that + // assert `reason === 'import-resolved'` keep working. The + // construction-site marker is opt-in for the same reason. + reason: constructionSiteReason( + fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call', + site, + options.markConstructionSites, + ), + }, }); emitted++; } + for (const { rel, gatedAll } of pending.values()) { + graph.addRelationship(gatedAll ? { ...rel, staticGated: true } : rel); + } } return emitted; } diff --git a/gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/main.zig b/gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/main.zig index 58d1c99cf..5cb821b63 100644 --- a/gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/main.zig +++ b/gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/main.zig @@ -162,6 +162,22 @@ pub fn run() void { if (cfg.NOT_A_BOOL != 0) { live_cross_file_not_bool(); } + + // Bare literal gate: no constant table involved, must still be gated + // (PR #3161 review, finding 1). + if (false) { + gated_bare_literal(); + } + + // Same callee reached from a LIVE site and a GATED site in one caller: the + // free-call edge is deduplicated per (caller, callee), so the flag must be + // the AND over all sites, never whichever site was visited first + // (PR #3161 review, finding 2). Live first here, gated first in + // `run_gated_first` below. + live_and_gated_same_callee(); + if (UPGRADERS_ENABLED) { + live_and_gated_same_callee(); + } } fn live_unconditional() void { @@ -295,3 +311,22 @@ fn live_cross_file_not_bool() void { fn some_runtime_flag() bool { return false; } + +pub fn run_gated_first() void { + if (UPGRADERS_ENABLED) { + gated_then_live_same_callee(); + } + gated_then_live_same_callee(); +} + +fn gated_bare_literal() void { + _ = 1; +} + +fn live_and_gated_same_callee() void { + _ = 1; +} + +fn gated_then_live_same_callee() void { + _ = 1; +} diff --git a/gitnexus/test/integration/resolvers/zig-static-gating.test.ts b/gitnexus/test/integration/resolvers/zig-static-gating.test.ts index 258d92d7f..cc2e96461 100644 --- a/gitnexus/test/integration/resolvers/zig-static-gating.test.ts +++ b/gitnexus/test/integration/resolvers/zig-static-gating.test.ts @@ -40,6 +40,18 @@ describe('Zig static-gated edges', () => { expect(isGated('gated_or_both_false')).toBe(true); }); + it('tags a bare `if (false)` gate even when the file declares no bool constants', () => { + expect(isGated('gated_bare_literal')).toBe(true); + }); + + it('keeps a deduplicated edge LIVE when a live site precedes a gated site', () => { + expect(isGated('live_and_gated_same_callee')).toBe(false); + }); + + it('keeps a deduplicated edge LIVE when a gated site precedes a live site', () => { + expect(isGated('gated_then_live_same_callee')).toBe(false); + }); + it('does NOT tag unconditional calls', () => { expect(isGated('live_unconditional')).toBe(false); });