Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of maybe stack in recursiveTypeRelatedTo #55224

Merged
merged 10 commits into from
Aug 3, 2023
40 changes: 24 additions & 16 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
createPrinterWithRemoveCommentsNeverAsciiEscape,
createPrinterWithRemoveCommentsOmitTrailingSemicolon,
createPropertyNameNodeForIdentifierOrLiteral,
createStackSet,
createSymbolTable,
createTextWriter,
Debug,
Expand Down Expand Up @@ -962,6 +963,7 @@ import {
SourceFile,
SpreadAssignment,
SpreadElement,
StackSet,
startsWith,
Statement,
stringContains,
Expand Down Expand Up @@ -20348,10 +20350,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

let errorInfo: DiagnosticMessageChain | undefined;
let relatedInfo: [DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined;
let maybeKeys: string[];
let maybeKeys: StackSet<string>;
let sourceStack: Type[];
let targetStack: Type[];
let maybeCount = 0;
let sourceDepth = 0;
let targetDepth = 0;
let expandingFlags = ExpandingFlags.None;
Expand Down Expand Up @@ -21224,29 +21225,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
if (!maybeKeys) {
maybeKeys = [];
maybeKeys = createStackSet();
sourceStack = [];
targetStack = [];
}
else {
// If source and target are already being compared, consider them related with assumptions
if (maybeKeys.has(id)) {
return Ternary.Maybe;
}

// A key that starts with "*" is an indication that we have type references that reference constrained
// type parameters. For such keys we also check against the key we would have gotten if all type parameters
// were unconstrained.
const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, intersectionState, relation, /*ignoreConstraints*/ true) : undefined;
for (let i = 0; i < maybeCount; i++) {
// If source and target are already being compared, consider them related with assumptions
if (id === maybeKeys[i] || broadestEquivalentId && broadestEquivalentId === maybeKeys[i]) {
return Ternary.Maybe;
}
if (broadestEquivalentId && maybeKeys.has(broadestEquivalentId)) {
return Ternary.Maybe;
}

if (sourceDepth === 100 || targetDepth === 100) {
overflow = true;
return Ternary.False;
}
}
const maybeStart = maybeCount;
maybeKeys[maybeCount] = id;
maybeCount++;
maybeKeys.push(id);
const saveExpandingFlags = expandingFlags;
if (recursionFlags & RecursionFlags.Source) {
sourceStack[sourceDepth] = source;
Expand Down Expand Up @@ -21301,18 +21303,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (result === Ternary.True || result === Ternary.Maybe) {
// If result is definitely true, record all maybe keys as having succeeded. Also, record Ternary.Maybe
// results as having succeeded once we reach depth 0, but never record Ternary.Unknown results.
for (let i = maybeStart; i < maybeCount; i++) {
relation.set(maybeKeys[i], RelationComparisonResult.Succeeded | propagatingVarianceFlags);
}
maybeKeys.popAll(v => {
jakebailey marked this conversation as resolved.
Show resolved Hide resolved
relation.set(v, RelationComparisonResult.Succeeded | propagatingVarianceFlags);
return v === id;
});
}
else {
maybeKeys.popAll(v => v === id);
}
maybeCount = maybeStart;
}
// Note: it's intentional that we don't pop in the else case;
// we leave them on the stack such that when we hit depth zero
// above, we can report all of them as successful.
}
else {
// A false result goes straight into global cache (when something is false under
// assumptions it will also be false without assumptions)
relation.set(id, (reportErrors ? RelationComparisonResult.Reported : 0) | RelationComparisonResult.Failed | propagatingVarianceFlags);
maybeCount = maybeStart;
maybeKeys.popAll(v => v === id);
}
return result;
}
Expand Down
53 changes: 53 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2882,3 +2882,56 @@ export function isNodeLikeSystem(): boolean {
&& !(process as any).browser
&& typeof module === "object";
}


/** @internal */
export interface StackSet<T extends {}> {
/**
* Returns true if the value has already been pushed.
*/
has(value: T): boolean;
/**
* Pushes to the stack. The value pushed must not have been pushed before.
jakebailey marked this conversation as resolved.
Show resolved Hide resolved
*/
push(value: T): void;
/**
* This pops from the stack until the callback returns true.
*
* Note that a value is popped, _then_ the callback is evaluated, meaning
* that if you return true to stop iteration, the value that the callback
* returned true for will still have been popped.
*/
popAll(callback: (v: T) => boolean): void;
}

/** @internal */
export function createStackSet<T extends {}>(): StackSet<T> {
// Why var? It avoids TDZ checks in the runtime which can be costly.
jakebailey marked this conversation as resolved.
Show resolved Hide resolved
// See: https://github.com/microsoft/TypeScript/issues/52924
/* eslint-disable no-var */
var set = new Set<T>();
var stack: T[] = [];
var end = 0;
/* eslint-enable no-var */

return {
has(value) {
return set.has(value);
jakebailey marked this conversation as resolved.
Show resolved Hide resolved
},
push(value) {
set.add(value);
stack[end] = value;
end++;
},
popAll(callback) {
while (end > 0) {
end--;
const value = stack[end];
set.delete(value);
if (callback(value)) {
break;
}
}
},
};
}