fix(cfg): bind Swift switch-case value patterns (case let n) (#2195)

A switch value-binding (case let n where …, case .some(let v)) was never
declared in prescan, so n/v resolved to a synthetic module binding and a body
use(n) did not link to any def — a very common Swift idiom silently lost its
data dependence. Declare the switch_pattern's bindings (prescan, reusing
declarePattern) and emit them as MAY-defs on the dispatch block (a case may not
match) via a new switchPatternFacts, propagated into the case body. swift suite
25 passed, tsc + grammar gate green. (The rare ?? / ternary-arm may-def — Swift
assignment-as-expression — remains a separate follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 19:39:32 +00:00
parent 324bde0368
commit c97d1b2dfc
3 changed files with 44 additions and 0 deletions

View file

@ -196,6 +196,13 @@ export class SwiftHarvester {
if (err) this.declarePattern(err);
break;
}
case 'switch_pattern': {
// `case let n` / `case (let a, let b)` / `case .some(let v)` — declare
// the value binding(s) so a body use resolves to a real local.
const pat = node.namedChildren.find((c) => c.type === 'pattern');
if (pat) this.declarePattern(pat);
break;
}
default:
// Optional binding (`if let` / `while let` / `guard let`): a
// `value_binding_pattern` condition followed by a `bound_identifier`.
@ -264,6 +271,19 @@ export class SwiftHarvester {
return acc.finish();
}
/**
* MAY-def facts for a `switch_pattern`'s value bindings (`case let n` /
* `case .some(let v)`). The binding only takes effect when the case matches,
* so it is a may-def on the dispatch block — propagated into the case body
* where the bound name is read.
*/
switchPatternFacts(switchPattern: SyntaxNode): StatementFacts {
const acc = new FactAccumulator(switchPattern.startPosition.row + 1);
const pat = switchPattern.namedChildren.find((c) => c.type === 'pattern');
if (pat) this.conditional(() => this.defPattern(pat, acc));
return acc.finish();
}
/**
* Facts for a `for item in COLLECTION` head: the loop pattern's leaves are
* defs, the iterated collection a use. The `where` guard (if any) is harvested

View file

@ -616,6 +616,10 @@ class SwiftCfgWalk {
// uses onto the dispatch block (a later case tests only when earlier ones
// didn't match; any def there is a may-def).
for (const entry of entries) {
// `case let n` value bindings are may-defs on the dispatch block (the case
// may not match) — propagated into the body where the name is read.
const pat = this.entryPattern(entry);
if (pat) this.builder.attachFacts(dispatch, this.harvest.switchPatternFacts(pat));
const guard = this.entryGuard(entry);
if (guard) this.builder.attachFacts(dispatch, this.harvest.factsConditional(guard));
}
@ -650,6 +654,11 @@ class SwiftCfgWalk {
return { entry: dispatch, exits: [switchExit] };
}
/** The `switch_pattern` of a `switch_entry` (absent for `default:`). */
private entryPattern(entry: SyntaxNode): SyntaxNode | undefined {
return entry.namedChildren.find((c) => c.type === 'switch_pattern');
}
/** The `where` guard expression of a `switch_entry`, if any. */
private entryGuard(entry: SyntaxNode): SyntaxNode | undefined {
let sawWhere = false;

View file

@ -292,6 +292,21 @@ describe('Swift CfgVisitor — def/use harvest', () => {
expect(cfgs.length).toBeGreaterThanOrEqual(2);
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
});
it('switch `case let n` binds n as a (may-)def, not a synthetic global (#2195 P2)', () => {
const cfg = swift.cfgOf(
`func f(x: Int) { switch x { case let n where n > 0: use(n); default: break } }`,
);
const n = bindingIdx(cfg, 'n');
// The case value-binding defines n (a may-def — the case may not match);
// the bug left n a synthetic module binding with no def at all.
const defined = cfg.blocks.some((bl) =>
bl.statements?.some((s) => s.defs.includes(n) || (s.mayDefs ?? []).includes(n)),
);
expect(defined).toBe(true);
// use(n) (and the `where n > 0` guard) read n.
expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(n)))).toBe(true);
});
});
describe('Swift CfgVisitor — functionStartColumn', () => {