Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,13 @@ const enum IntrinsicTypeKind {
NoInfer,
}

const enum FlowNodeReachableCacheFlags {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
ReadWrite = Read | Write,
}

const intrinsicTypeKinds: ReadonlyMap<string, IntrinsicTypeKind> = new Map(Object.entries({
Uppercase: IntrinsicTypeKind.Uppercase,
Lowercase: IntrinsicTypeKind.Lowercase,
Expand Down Expand Up @@ -28104,7 +28111,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function isReachableFlowNode(flow: FlowNode) {
const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false);
const result = isReachableFlowNodeWorker(flow, FlowNodeReachableCacheFlags.ReadWrite);
lastFlowNode = flow;
lastFlowNodeReachable = result;
return result;
Expand All @@ -28118,19 +28125,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
);
}

function isReachableFlowNodeWorker(flow: FlowNode, noCacheCheck: boolean): boolean {
function isReachableFlowNodeWorker(flow: FlowNode, cacheFlags: FlowNodeReachableCacheFlags): boolean {
while (true) {
if (flow === lastFlowNode) {
return lastFlowNodeReachable;
}
const flags = flow.flags;
if (flags & FlowFlags.Shared) {
if (!noCacheCheck) {
if (cacheFlags & FlowNodeReachableCacheFlags.Read) {
const id = getFlowNodeId(flow);
const reachable = flowNodeReachable[id];
return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true));
let reachable = flowNodeReachable[id];
if (reachable !== undefined) {
return reachable;
}
reachable = isReachableFlowNodeWorker(flow, cacheFlags & ~FlowNodeReachableCacheFlags.Read);
if (cacheFlags & FlowNodeReachableCacheFlags.Write) {
flowNodeReachable[id] = reachable;
}
return reachable;
}
noCacheCheck = false;
cacheFlags |= FlowNodeReachableCacheFlags.Read;
}
if (flags & (FlowFlags.Assignment | FlowFlags.Condition | FlowFlags.ArrayMutation)) {
flow = (flow as FlowAssignment | FlowCondition | FlowArrayMutation).antecedent;
Expand All @@ -28153,7 +28167,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
else if (flags & FlowFlags.BranchLabel) {
// A branching point is reachable if any branch is reachable.
return some((flow as FlowLabel).antecedent, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false));
return some((flow as FlowLabel).antecedent, f => isReachableFlowNodeWorker(f, cacheFlags));
}
else if (flags & FlowFlags.LoopLabel) {
const antecedents = (flow as FlowLabel).antecedent;
Expand All @@ -28178,7 +28192,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const target = (flow as FlowReduceLabel).node.target;
const saveAntecedents = target.antecedent;
target.antecedent = (flow as FlowReduceLabel).node.antecedents;
const result = isReachableFlowNodeWorker((flow as FlowReduceLabel).antecedent, /*noCacheCheck*/ false);
const result = isReachableFlowNodeWorker((flow as FlowReduceLabel).antecedent, FlowNodeReachableCacheFlags.None);
Copy link
Contributor Author

@Andarist Andarist Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as the comment in this block says:

// Cache is unreliable once we start adjusting labels

We can't let this isReachableFlowNodeWorker call here to populate the cache as that would create entries based on the temporary antecedent(s). Anything computed below this call can't be preserved, at least not without including the id of this ReduceLabel flow in the cache key or smth.

So this disables cache writes. As later on that would be used on the same shared flow but with "correct" antecedents (and that's the only result the compiler should cache and trust).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still considering 2 things here:

  • should this code maybe save & restore flowNodeReachable[getFlowNodeId(target)]?
  • should reads be permanently disabled here? this would make the previous consideration obsolete

I have not been able to produce a test case that would prove either to be required so far but I have only spent a little bit of time on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @ahejlsberg who has been thinking about this sort of thing recently and I believe had ideas on how to simplify the flow node logic here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, Corsa still suffers from this (checked at microsoft/typescript-go@a92eff4 )

target.antecedent = saveAntecedents;
return result;
}
Expand Down
113 changes: 113 additions & 0 deletions tests/baselines/reference/reachabilityChecks9.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//// [tests/cases/compiler/reachabilityChecks9.ts] ////

=== reachabilityChecks9.ts ===
// https://github.com/microsoft/TypeScript/issues/61259

const a = (v: 1 | 2) => {
>a : Symbol(a, Decl(reachabilityChecks9.ts, 2, 5))
>v : Symbol(v, Decl(reachabilityChecks9.ts, 2, 11))

try {
switch (v) {
>v : Symbol(v, Decl(reachabilityChecks9.ts, 2, 11))

case 1:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 2, 11))

case 2:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 2, 11))
}
} finally {
console.log("exit");
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
}
};

const b = (v: number) => {
>b : Symbol(b, Decl(reachabilityChecks9.ts, 15, 5))
>v : Symbol(v, Decl(reachabilityChecks9.ts, 15, 11))

try {
switch (v) {
>v : Symbol(v, Decl(reachabilityChecks9.ts, 15, 11))

case 1:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 15, 11))

default:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 15, 11))
}
} finally {
console.log("exit");
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
}
};

const c = (v: 1 | 2) => {
>c : Symbol(c, Decl(reachabilityChecks9.ts, 28, 5))
>v : Symbol(v, Decl(reachabilityChecks9.ts, 28, 11))

try {
switch (v) {
>v : Symbol(v, Decl(reachabilityChecks9.ts, 28, 11))

case 1:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 28, 11))

case 2:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 28, 11))
}
} finally {
if (Math.random()) {
>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))
>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))

console.log("exit");
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
}
}
};

const d = (v: number) => {
>d : Symbol(d, Decl(reachabilityChecks9.ts, 43, 5))
>v : Symbol(v, Decl(reachabilityChecks9.ts, 43, 11))

try {
switch (v) {
>v : Symbol(v, Decl(reachabilityChecks9.ts, 43, 11))

case 1:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 43, 11))

default:
return v;
>v : Symbol(v, Decl(reachabilityChecks9.ts, 43, 11))
}
} finally {
if (Math.random()) {
>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))
>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))

console.log("exit");
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
}
}
};

197 changes: 197 additions & 0 deletions tests/baselines/reference/reachabilityChecks9.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//// [tests/cases/compiler/reachabilityChecks9.ts] ////

=== reachabilityChecks9.ts ===
// https://github.com/microsoft/TypeScript/issues/61259

const a = (v: 1 | 2) => {
>a : (v: 1 | 2) => 1 | 2
> : ^ ^^ ^^^^^^^^^^
>(v: 1 | 2) => { try { switch (v) { case 1: return v; case 2: return v; } } finally { console.log("exit"); }} : (v: 1 | 2) => 1 | 2
> : ^ ^^ ^^^^^^^^^^
>v : 1 | 2
> : ^^^^^

try {
switch (v) {
>v : 1 | 2
> : ^^^^^

case 1:
>1 : 1
> : ^

return v;
>v : 1
> : ^

case 2:
>2 : 2
> : ^

return v;
>v : 2
> : ^
}
} finally {
console.log("exit");
>console.log("exit") : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>"exit" : "exit"
> : ^^^^^^
}
};

const b = (v: number) => {
>b : (v: number) => number
> : ^ ^^ ^^^^^^^^^^^
>(v: number) => { try { switch (v) { case 1: return v; default: return v; } } finally { console.log("exit"); }} : (v: number) => number
> : ^ ^^ ^^^^^^^^^^^
>v : number
> : ^^^^^^

try {
switch (v) {
>v : number
> : ^^^^^^

case 1:
>1 : 1
> : ^

return v;
>v : 1
> : ^

default:
return v;
>v : number
> : ^^^^^^
}
} finally {
console.log("exit");
>console.log("exit") : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>"exit" : "exit"
> : ^^^^^^
}
};

const c = (v: 1 | 2) => {
>c : (v: 1 | 2) => 1 | 2
> : ^ ^^ ^^^^^^^^^^
>(v: 1 | 2) => { try { switch (v) { case 1: return v; case 2: return v; } } finally { if (Math.random()) { console.log("exit"); } }} : (v: 1 | 2) => 1 | 2
> : ^ ^^ ^^^^^^^^^^
>v : 1 | 2
> : ^^^^^

try {
switch (v) {
>v : 1 | 2
> : ^^^^^

case 1:
>1 : 1
> : ^

return v;
>v : 1
> : ^

case 2:
>2 : 2
> : ^

return v;
>v : 2
> : ^
}
} finally {
if (Math.random()) {
>Math.random() : number
> : ^^^^^^
>Math.random : () => number
> : ^^^^^^
>Math : Math
> : ^^^^
>random : () => number
> : ^^^^^^

console.log("exit");
>console.log("exit") : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>"exit" : "exit"
> : ^^^^^^
}
}
};

const d = (v: number) => {
>d : (v: number) => number
> : ^ ^^ ^^^^^^^^^^^
>(v: number) => { try { switch (v) { case 1: return v; default: return v; } } finally { if (Math.random()) { console.log("exit"); } }} : (v: number) => number
> : ^ ^^ ^^^^^^^^^^^
>v : number
> : ^^^^^^

try {
switch (v) {
>v : number
> : ^^^^^^

case 1:
>1 : 1
> : ^

return v;
>v : 1
> : ^

default:
return v;
>v : number
> : ^^^^^^
}
} finally {
if (Math.random()) {
>Math.random() : number
> : ^^^^^^
>Math.random : () => number
> : ^^^^^^
>Math : Math
> : ^^^^
>random : () => number
> : ^^^^^^

console.log("exit");
>console.log("exit") : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>"exit" : "exit"
> : ^^^^^^
}
}
};

Loading
Loading