From c450425b41872e479a14f524693148741c9dab33 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 1 Mar 2021 18:13:45 -0800 Subject: [PATCH] Use shared binary trampoline in checker --- src/compiler/checker.ts | 195 +++++++++++++++++++----------- src/compiler/factory/utilities.ts | 57 ++++++--- 2 files changed, 165 insertions(+), 87 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9d8edbe12a5fa..4068cd51b1dd5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -357,6 +357,7 @@ namespace ts { const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral; + const checkBinaryExpression = createCheckBinaryExpression(); const emitResolver = createResolver(); const nodeBuilder = createNodeBuilder(); @@ -30981,92 +30982,142 @@ namespace ts { return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target); } - const enum CheckBinaryExpressionState { - MaybeCheckLeft, - CheckRight, - FinishCheck - } + function createCheckBinaryExpression() { + interface WorkArea { + readonly checkMode: CheckMode | undefined; + skip: boolean; + stackIndex: number; + /** + * Holds the types from the left-side of an expression from [0..stackIndex]. + * Holds the type of the result at stackIndex+1. This allows us to reuse existing stack entries + * and avoid storing an extra property on the object (i.e., `lastResult`). + */ + typeStack: (Type | undefined)[]; + } - function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) { - const workStacks: { - expr: BinaryExpression[], - state: CheckBinaryExpressionState[], - leftType: (Type | undefined)[] - } = { - expr: [node], - state: [CheckBinaryExpressionState.MaybeCheckLeft], - leftType: [undefined] + const trampoline = createBinaryExpressionTrampoline(onEnter, onLeft, onOperator, onRight, onExit, foldState); + + return (node: BinaryExpression, checkMode: CheckMode | undefined) => { + const result = trampoline(node, checkMode); + Debug.assertIsDefined(result); + return result; }; - let stackIndex = 0; - let lastResult: Type | undefined; - while (stackIndex >= 0) { - node = workStacks.expr[stackIndex]; - switch (workStacks.state[stackIndex]) { - case CheckBinaryExpressionState.MaybeCheckLeft: { - if (isInJSFile(node) && getAssignedExpandoInitializer(node)) { - finishInvocation(checkExpression(node.right, checkMode)); - break; - } - checkGrammarNullishCoalesceWithLogicalExpression(node); - const operator = node.operatorToken.kind; - if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { - finishInvocation(checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); - break; - } - advanceState(CheckBinaryExpressionState.CheckRight); - maybeCheckExpression(node.left); - break; - } - case CheckBinaryExpressionState.CheckRight: { - const leftType = lastResult!; - workStacks.leftType[stackIndex] = leftType; - const operator = node.operatorToken.kind; - if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { - if (operator === SyntaxKind.AmpersandAmpersandToken) { - const parent = walkUpParenthesizedExpressions(node.parent); - checkTestingKnownTruthyCallableOrAwaitableType(node.left, leftType, isIfStatement(parent) ? parent.thenStatement : undefined); - } - checkTruthinessOfType(leftType, node.left); + + function onEnter(node: BinaryExpression, state: WorkArea | undefined, checkMode: CheckMode | undefined) { + if (state) { + state.stackIndex++; + state.skip = false; + setLeftType(state, /*type*/ undefined); + setLastResult(state, /*type*/ undefined); + } + else { + state = { + checkMode, + skip: false, + stackIndex: 0, + typeStack: [undefined, undefined], + }; + } + + if (isInJSFile(node) && getAssignedExpandoInitializer(node)) { + state.skip = true; + setLastResult(state, checkExpression(node.right, checkMode)); + return state; + } + + checkGrammarNullishCoalesceWithLogicalExpression(node); + + const operator = node.operatorToken.kind; + if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { + state.skip = true; + setLastResult(state, checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); + return state; + } + + return state; + } + + function onLeft(left: Expression, state: WorkArea, _node: BinaryExpression) { + if (!state.skip) { + return maybeCheckExpression(state, left); + } + } + + function onOperator(operatorToken: BinaryOperatorToken, state: WorkArea, node: BinaryExpression) { + if (!state.skip) { + const leftType = getLastResult(state); + Debug.assertIsDefined(leftType); + setLeftType(state, leftType); + setLastResult(state, /*type*/ undefined); + const operator = operatorToken.kind; + if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { + if (operator === SyntaxKind.AmpersandAmpersandToken) { + const parent = walkUpParenthesizedExpressions(node.parent); + checkTestingKnownTruthyCallableOrAwaitableType(node.left, leftType, isIfStatement(parent) ? parent.thenStatement : undefined); } - advanceState(CheckBinaryExpressionState.FinishCheck); - maybeCheckExpression(node.right); - break; - } - case CheckBinaryExpressionState.FinishCheck: { - const leftType = workStacks.leftType[stackIndex]!; - const rightType = lastResult!; - finishInvocation(checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node)); - break; + checkTruthinessOfType(leftType, node.left); } - default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for checkBinaryExpression`); } } - return lastResult!; + function onRight(right: Expression, state: WorkArea, _node: BinaryExpression) { + if (!state.skip) { + return maybeCheckExpression(state, right); + } + } + + function onExit(node: BinaryExpression, state: WorkArea): Type | undefined { + let result: Type | undefined; + if (state.skip) { + result = getLastResult(state); + } + else { + const leftType = getLeftType(state); + Debug.assertIsDefined(leftType); + + const rightType = getLastResult(state); + Debug.assertIsDefined(rightType); - function finishInvocation(result: Type) { - lastResult = result; - stackIndex--; + result = checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node); + } + + state.skip = false; + setLeftType(state, /*type*/ undefined); + setLastResult(state, /*type*/ undefined); + state.stackIndex--; + return result; } - /** - * Note that `advanceState` sets the _current_ head state, and that `maybeCheckExpression` potentially pushes on a new - * head state; so `advanceState` must be called before any `maybeCheckExpression` during a state's execution. - */ - function advanceState(nextState: CheckBinaryExpressionState) { - workStacks.state[stackIndex] = nextState; + function foldState(state: WorkArea, result: Type | undefined, _side: "left" | "right") { + setLastResult(state, result); + return state; } - function maybeCheckExpression(node: Expression) { + function maybeCheckExpression(state: WorkArea, node: Expression): BinaryExpression | undefined { if (isBinaryExpression(node)) { - stackIndex++; - workStacks.expr[stackIndex] = node; - workStacks.state[stackIndex] = CheckBinaryExpressionState.MaybeCheckLeft; - workStacks.leftType[stackIndex] = undefined; - } - else { - lastResult = checkExpression(node, checkMode); + return node; } + setLastResult(state, checkExpression(node, state.checkMode)); + } + + function getLeftType(state: WorkArea) { + return state.typeStack[state.stackIndex]; + } + + function setLeftType(state: WorkArea, type: Type | undefined) { + state.typeStack[state.stackIndex] = type; + } + + function getLastResult(state: WorkArea) { + return state.typeStack[state.stackIndex + 1]; + } + + function setLastResult(state: WorkArea, type: Type | undefined) { + // To reduce overhead, reuse the next stack entry to store the + // last result. This avoids the overhead of an additional property + // on `WorkArea` and reuses empty stack entries as we walk back up + // the stack. + state.typeStack[state.stackIndex + 1] = type; } } diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index 357e50d7fa58b..433f13c5c3e29 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -930,7 +930,7 @@ namespace ts { return isBinaryOperator(node.kind); } - type BinaryExpressionState = (machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], resultHolder: { value: TResult }) => number; + type BinaryExpressionState = (machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], resultHolder: { value: TResult }, outerState: TOuterState) => number; namespace BinaryExpressionState { /** @@ -939,10 +939,10 @@ namespace ts { * @param frame The current frame * @returns The new frame */ - export function enter(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }): number { + export function enter(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }, outerState: TOuterState): number { const prevUserState = stackIndex > 0 ? userStateStack[stackIndex - 1] : undefined; Debug.assertEqual(stateStack[stackIndex], enter); - userStateStack[stackIndex] = machine.onEnter(nodeStack[stackIndex], prevUserState); + userStateStack[stackIndex] = machine.onEnter(nodeStack[stackIndex], prevUserState, outerState); stateStack[stackIndex] = nextState(machine, enter); return stackIndex; } @@ -953,7 +953,7 @@ namespace ts { * @param frame The current frame * @returns The new frame */ - export function left(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }): number { + export function left(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }, _outerState: TOuterState): number { Debug.assertEqual(stateStack[stackIndex], left); Debug.assertIsDefined(machine.onLeft); stateStack[stackIndex] = nextState(machine, left); @@ -971,7 +971,7 @@ namespace ts { * @param frame The current frame * @returns The new frame */ - export function operator(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }): number { + export function operator(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }, _outerState: TOuterState): number { Debug.assertEqual(stateStack[stackIndex], operator); Debug.assertIsDefined(machine.onOperator); stateStack[stackIndex] = nextState(machine, operator); @@ -985,7 +985,7 @@ namespace ts { * @param frame The current frame * @returns The new frame */ - export function right(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }): number { + export function right(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], _resultHolder: { value: TResult }, _outerState: TOuterState): number { Debug.assertEqual(stateStack[stackIndex], right); Debug.assertIsDefined(machine.onRight); stateStack[stackIndex] = nextState(machine, right); @@ -1003,7 +1003,7 @@ namespace ts { * @param frame The current frame * @returns The new frame */ - export function exit(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], resultHolder: { value: TResult }): number { + export function exit(machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], nodeStack: BinaryExpression[], userStateStack: TState[], resultHolder: { value: TResult }, _outerState: TOuterState): number { Debug.assertEqual(stateStack[stackIndex], exit); stateStack[stackIndex] = nextState(machine, exit); const result = machine.onExit(nodeStack[stackIndex], userStateStack[stackIndex]); @@ -1024,12 +1024,12 @@ namespace ts { * Handles a frame that is already done. * @returns The `done` state. */ - export function done(_machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], _nodeStack: BinaryExpression[], _userStateStack: TState[], _resultHolder: { value: TResult }): number { + export function done(_machine: BinaryExpressionStateMachine, stackIndex: number, stateStack: BinaryExpressionState[], _nodeStack: BinaryExpression[], _userStateStack: TState[], _resultHolder: { value: TResult }, _outerState: TOuterState): number { Debug.assertEqual(stateStack[stackIndex], done); return stackIndex; } - export function nextState(machine: BinaryExpressionStateMachine, currentState: BinaryExpressionState) { + export function nextState(machine: BinaryExpressionStateMachine, currentState: BinaryExpressionState) { switch (currentState) { case enter: if (machine.onLeft) return left; @@ -1068,9 +1068,9 @@ namespace ts { /** * Holds state machine handler functions */ - class BinaryExpressionStateMachine { + class BinaryExpressionStateMachine { constructor( - readonly onEnter: (node: BinaryExpression, prev: TState | undefined) => TState, + readonly onEnter: (node: BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, readonly onLeft: ((left: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, readonly onOperator: ((operatorToken: BinaryOperatorToken, userState: TState, node: BinaryExpression) => void) | undefined, readonly onRight: ((right: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, @@ -1089,26 +1089,53 @@ namespace ts { * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. */ - export function createBinaryExpressionTrampoline( + export function createBinaryExpressionTrampoline( onEnter: (node: BinaryExpression, prev: TState | undefined) => TState, onLeft: ((left: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, onOperator: ((operatorToken: BinaryOperatorToken, userState: TState, node: BinaryExpression) => void) | undefined, onRight: ((right: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, onExit: (node: BinaryExpression, userState: TState) => TResult, foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined, + ): (node: BinaryExpression) => TResult; + /** + * Creates a state machine that walks a `BinaryExpression` using the heap to reduce call-stack depth on a large tree. + * @param onEnter Callback evaluated when entering a `BinaryExpression`. Returns new user-defined state to associate with the node while walking. + * @param onLeft Callback evaluated when walking the left side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the right side. + * @param onRight Callback evaluated when walking the right side of a `BinaryExpression`. Return a `BinaryExpression` to continue walking, or `void` to advance to the end of the node. + * @param onExit Callback evaluated when exiting a `BinaryExpression`. The result returned will either be folded into the parent's state, or returned from the walker if at the top frame. + * @param foldState Callback evaluated when the result from a nested `onExit` should be folded into the state of that node's parent. + * @returns A function that walks a `BinaryExpression` node using the above callbacks, returning the result of the call to `onExit` from the outermost `BinaryExpression` node. + */ + export function createBinaryExpressionTrampoline( + onEnter: (node: BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, + onLeft: ((left: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, + onOperator: ((operatorToken: BinaryOperatorToken, userState: TState, node: BinaryExpression) => void) | undefined, + onRight: ((right: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, + onExit: (node: BinaryExpression, userState: TState) => TResult, + foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined, + ): (node: BinaryExpression, outerState: TOuterState) => TResult; + export function createBinaryExpressionTrampoline( + onEnter: (node: BinaryExpression, prev: TState | undefined, outerState: TOuterState) => TState, + onLeft: ((left: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, + onOperator: ((operatorToken: BinaryOperatorToken, userState: TState, node: BinaryExpression) => void) | undefined, + onRight: ((right: Expression, userState: TState, node: BinaryExpression) => BinaryExpression | void) | undefined, + onExit: (node: BinaryExpression, userState: TState) => TResult, + foldState: ((userState: TState, result: TResult, side: "left" | "right") => TState) | undefined, ) { const machine = new BinaryExpressionStateMachine(onEnter, onLeft, onOperator, onRight, onExit, foldState); - return (node: BinaryExpression) => { + return trampoline; + + function trampoline(node: BinaryExpression, outerState?: TOuterState) { const resultHolder: { value: TResult } = { value: undefined! }; const stateStack: BinaryExpressionState[] = [BinaryExpressionState.enter]; const nodeStack: BinaryExpression[] = [node]; const userStateStack: TState[] = [undefined!]; let stackIndex = 0; while (stateStack[stackIndex] !== BinaryExpressionState.done) { - stackIndex = stateStack[stackIndex](machine, stackIndex, stateStack, nodeStack, userStateStack, resultHolder); + stackIndex = stateStack[stackIndex](machine, stackIndex, stateStack, nodeStack, userStateStack, resultHolder, outerState); } Debug.assertEqual(stackIndex, 0); return resultHolder.value; - }; + } } }