Skip to content

Commit 08e6bc2

Browse files
authored
Trampolines for large binary expressions (microsoft#36248)
* WIP * Test no longer crashes, but emit trampoline is incomplete and skips pipeline phases * Fix lints, use non-generator trampoline in emit (still skips pipeline) * Final version with emitBinaryExprssion work stack that is only used if possible * Fix lints * retarget to es2015 for testing * Use bespoke state machine trampolines in binder and checker * Remove now extraneous code in parser * Adjust fixupParentReferences to use a depth first preorder traversal rather than breadth first * Reintroduce incremental fast bail in fixupParentReferences * Revert target to es5 * PR feedback * Small edit for devops rebuild with updated definition * Fix comment nits, add internally extraneous check back into transformer
1 parent f3cc6f6 commit 08e6bc2

File tree

14 files changed

+15373
-129
lines changed

14 files changed

+15373
-129
lines changed

src/compiler/binder.ts

Lines changed: 145 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,28 +1445,156 @@ namespace ts {
14451445
}
14461446
}
14471447

1448+
const enum BindBinaryExpressionFlowState {
1449+
BindThenBindChildren,
1450+
MaybeBindLeft,
1451+
BindToken,
1452+
BindRight,
1453+
FinishBind
1454+
}
1455+
14481456
function bindBinaryExpressionFlow(node: BinaryExpression) {
1449-
const operator = node.operatorToken.kind;
1450-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
1451-
if (isTopLevelLogicalExpression(node)) {
1452-
const postExpressionLabel = createBranchLabel();
1453-
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
1454-
currentFlow = finishFlowLabel(postExpressionLabel);
1457+
const workStacks: {
1458+
expr: BinaryExpression[],
1459+
state: BindBinaryExpressionFlowState[],
1460+
inStrictMode: (boolean | undefined)[],
1461+
parent: (Node | undefined)[],
1462+
subtreeFlags: (number | undefined)[]
1463+
} = {
1464+
expr: [node],
1465+
state: [BindBinaryExpressionFlowState.MaybeBindLeft],
1466+
inStrictMode: [undefined],
1467+
parent: [undefined],
1468+
subtreeFlags: [undefined]
1469+
};
1470+
let stackIndex = 0;
1471+
while (stackIndex >= 0) {
1472+
node = workStacks.expr[stackIndex];
1473+
switch (workStacks.state[stackIndex]) {
1474+
case BindBinaryExpressionFlowState.BindThenBindChildren: {
1475+
// This state is used only when recuring, to emulate the work that `bind` does before
1476+
// reaching `bindChildren`. A normal call to `bindBinaryExpressionFlow` will already have done this work.
1477+
node.parent = parent;
1478+
const saveInStrictMode = inStrictMode;
1479+
bindWorker(node);
1480+
const saveParent = parent;
1481+
parent = node;
1482+
1483+
let subtreeFlagsState: number | undefined;
1484+
// While this next part does the work of `bindChildren` before it descends into `bindChildrenWorker`
1485+
// and uses `subtreeFlagsState` to queue up the work that needs to be done once the node is bound.
1486+
if (skipTransformFlagAggregation) {
1487+
// do nothing extra
1488+
}
1489+
else if (node.transformFlags & TransformFlags.HasComputedFlags) {
1490+
skipTransformFlagAggregation = true;
1491+
subtreeFlagsState = -1;
1492+
}
1493+
else {
1494+
const savedSubtreeTransformFlags = subtreeTransformFlags;
1495+
subtreeTransformFlags = 0;
1496+
subtreeFlagsState = savedSubtreeTransformFlags;
1497+
}
1498+
1499+
advanceState(BindBinaryExpressionFlowState.MaybeBindLeft, saveInStrictMode, saveParent, subtreeFlagsState);
1500+
break;
1501+
}
1502+
case BindBinaryExpressionFlowState.MaybeBindLeft: {
1503+
const operator = node.operatorToken.kind;
1504+
// TODO: bindLogicalExpression is recursive - if we want to handle deeply nested `&&` expressions
1505+
// we'll need to handle the `bindLogicalExpression` scenarios in this state machine, too
1506+
// For now, though, since the common cases are chained `+`, leaving it recursive is fine
1507+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
1508+
if (isTopLevelLogicalExpression(node)) {
1509+
const postExpressionLabel = createBranchLabel();
1510+
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
1511+
currentFlow = finishFlowLabel(postExpressionLabel);
1512+
}
1513+
else {
1514+
bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!);
1515+
}
1516+
completeNode();
1517+
}
1518+
else {
1519+
advanceState(BindBinaryExpressionFlowState.BindToken);
1520+
maybeBind(node.left);
1521+
}
1522+
break;
1523+
}
1524+
case BindBinaryExpressionFlowState.BindToken: {
1525+
advanceState(BindBinaryExpressionFlowState.BindRight);
1526+
maybeBind(node.operatorToken);
1527+
break;
1528+
}
1529+
case BindBinaryExpressionFlowState.BindRight: {
1530+
advanceState(BindBinaryExpressionFlowState.FinishBind);
1531+
maybeBind(node.right);
1532+
break;
1533+
}
1534+
case BindBinaryExpressionFlowState.FinishBind: {
1535+
const operator = node.operatorToken.kind;
1536+
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
1537+
bindAssignmentTargetFlow(node.left);
1538+
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
1539+
const elementAccess = <ElementAccessExpression>node.left;
1540+
if (isNarrowableOperand(elementAccess.expression)) {
1541+
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
1542+
}
1543+
}
1544+
}
1545+
completeNode();
1546+
break;
1547+
}
1548+
default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for bindBinaryExpressionFlow`);
14551549
}
1456-
else {
1457-
bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!);
1550+
}
1551+
1552+
/**
1553+
* Note that `advanceState` sets the _current_ head state, and that `maybeBind` potentially pushes on a new
1554+
* head state; so `advanceState` must be called before any `maybeBind` during a state's execution.
1555+
*/
1556+
function advanceState(state: BindBinaryExpressionFlowState, isInStrictMode?: boolean, parent?: Node, subtreeFlags?: number) {
1557+
workStacks.state[stackIndex] = state;
1558+
if (isInStrictMode !== undefined) {
1559+
workStacks.inStrictMode[stackIndex] = isInStrictMode;
1560+
}
1561+
if (parent !== undefined) {
1562+
workStacks.parent[stackIndex] = parent;
1563+
}
1564+
if (subtreeFlags !== undefined) {
1565+
workStacks.subtreeFlags[stackIndex] = subtreeFlags;
14581566
}
14591567
}
1460-
else {
1461-
bindEachChild(node);
1462-
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
1463-
bindAssignmentTargetFlow(node.left);
1464-
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
1465-
const elementAccess = <ElementAccessExpression>node.left;
1466-
if (isNarrowableOperand(elementAccess.expression)) {
1467-
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
1468-
}
1568+
1569+
function completeNode() {
1570+
if (workStacks.inStrictMode[stackIndex] !== undefined) {
1571+
if (workStacks.subtreeFlags[stackIndex] === -1) {
1572+
skipTransformFlagAggregation = false;
1573+
subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind);
1574+
}
1575+
else if (workStacks.subtreeFlags[stackIndex] !== undefined) {
1576+
subtreeTransformFlags = workStacks.subtreeFlags[stackIndex]! | computeTransformFlagsForNode(node, subtreeTransformFlags);
14691577
}
1578+
inStrictMode = workStacks.inStrictMode[stackIndex]!;
1579+
parent = workStacks.parent[stackIndex]!;
1580+
}
1581+
stackIndex--;
1582+
}
1583+
1584+
/**
1585+
* If `node` is a BinaryExpression, adds it to the local work stack, otherwise recursively binds it
1586+
*/
1587+
function maybeBind(node: Node) {
1588+
if (node && isBinaryExpression(node)) {
1589+
stackIndex++;
1590+
workStacks.expr[stackIndex] = node;
1591+
workStacks.state[stackIndex] = BindBinaryExpressionFlowState.BindThenBindChildren;
1592+
workStacks.inStrictMode[stackIndex] = undefined;
1593+
workStacks.parent[stackIndex] = undefined;
1594+
workStacks.subtreeFlags[stackIndex] = undefined;
1595+
}
1596+
else {
1597+
bind(node);
14701598
}
14711599
}
14721600
}

src/compiler/checker.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27136,12 +27136,89 @@ namespace ts {
2713627136
return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target);
2713727137
}
2713827138

27139+
const enum CheckBinaryExpressionState {
27140+
MaybeCheckLeft,
27141+
CheckRight,
27142+
FinishCheck
27143+
}
27144+
2713927145
function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) {
27140-
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
27141-
return checkExpression(node.right, checkMode);
27146+
const workStacks: {
27147+
expr: BinaryExpression[],
27148+
state: CheckBinaryExpressionState[],
27149+
leftType: (Type | undefined)[]
27150+
} = {
27151+
expr: [node],
27152+
state: [CheckBinaryExpressionState.MaybeCheckLeft],
27153+
leftType: [undefined]
27154+
};
27155+
let stackIndex = 0;
27156+
let lastResult: Type | undefined;
27157+
while (stackIndex >= 0) {
27158+
node = workStacks.expr[stackIndex];
27159+
switch (workStacks.state[stackIndex]) {
27160+
case CheckBinaryExpressionState.MaybeCheckLeft: {
27161+
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
27162+
finishInvocation(checkExpression(node.right, checkMode));
27163+
break;
27164+
}
27165+
checkGrammarNullishCoalesceWithLogicalExpression(node);
27166+
const operator = node.operatorToken.kind;
27167+
if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) {
27168+
finishInvocation(checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword));
27169+
break;
27170+
}
27171+
advanceState(CheckBinaryExpressionState.CheckRight);
27172+
maybeCheckExpression(node.left);
27173+
break;
27174+
}
27175+
case CheckBinaryExpressionState.CheckRight: {
27176+
const leftType = lastResult!;
27177+
workStacks.leftType[stackIndex] = leftType;
27178+
const operator = node.operatorToken.kind;
27179+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
27180+
checkTruthinessOfType(leftType, node.left);
27181+
}
27182+
advanceState(CheckBinaryExpressionState.FinishCheck);
27183+
maybeCheckExpression(node.right);
27184+
break;
27185+
}
27186+
case CheckBinaryExpressionState.FinishCheck: {
27187+
const leftType = workStacks.leftType[stackIndex]!;
27188+
const rightType = lastResult!;
27189+
finishInvocation(checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node));
27190+
break;
27191+
}
27192+
default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for checkBinaryExpression`);
27193+
}
27194+
}
27195+
27196+
return lastResult!;
27197+
27198+
function finishInvocation(result: Type) {
27199+
lastResult = result;
27200+
stackIndex--;
27201+
}
27202+
27203+
/**
27204+
* Note that `advanceState` sets the _current_ head state, and that `maybeCheckExpression` potentially pushes on a new
27205+
* head state; so `advanceState` must be called before any `maybeCheckExpression` during a state's execution.
27206+
*/
27207+
function advanceState(nextState: CheckBinaryExpressionState) {
27208+
workStacks.state[stackIndex] = nextState;
27209+
}
27210+
27211+
function maybeCheckExpression(node: Expression) {
27212+
if (isBinaryExpression(node)) {
27213+
stackIndex++;
27214+
workStacks.expr[stackIndex] = node;
27215+
workStacks.state[stackIndex] = CheckBinaryExpressionState.MaybeCheckLeft;
27216+
workStacks.leftType[stackIndex] = undefined;
27217+
}
27218+
else {
27219+
lastResult = checkExpression(node, checkMode);
27220+
}
2714227221
}
27143-
checkGrammarNullishCoalesceWithLogicalExpression(node);
27144-
return checkBinaryLikeExpression(node.left, node.operatorToken, node.right, checkMode, node);
2714527222
}
2714627223

2714727224
function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) {
@@ -27156,6 +27233,8 @@ namespace ts {
2715627233
}
2715727234
}
2715827235

27236+
// Note that this and `checkBinaryExpression` above should behave mostly the same, except this elides some
27237+
// expression-wide checks and does not use a work stack to fold nested binary expressions into the same callstack frame
2715927238
function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): Type {
2716027239
const operator = operatorToken.kind;
2716127240
if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) {
@@ -27169,7 +27248,19 @@ namespace ts {
2716927248
leftType = checkExpression(left, checkMode);
2717027249
}
2717127250

27172-
let rightType = checkExpression(right, checkMode);
27251+
const rightType = checkExpression(right, checkMode);
27252+
return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode);
27253+
}
27254+
27255+
function checkBinaryLikeExpressionWorker(
27256+
left: Expression,
27257+
operatorToken: Node,
27258+
right: Expression,
27259+
leftType: Type,
27260+
rightType: Type,
27261+
errorNode?: Node
27262+
): Type {
27263+
const operator = operatorToken.kind;
2717327264
switch (operator) {
2717427265
case SyntaxKind.AsteriskToken:
2717527266
case SyntaxKind.AsteriskAsteriskToken:
@@ -30774,14 +30865,17 @@ namespace ts {
3077430865
checkSourceElement(node.statement);
3077530866
}
3077630867

30777-
function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) {
30778-
const type = checkExpression(node, checkMode);
30868+
function checkTruthinessOfType(type: Type, node: Node) {
3077930869
if (type.flags & TypeFlags.Void) {
3078030870
error(node, Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness);
3078130871
}
3078230872
return type;
3078330873
}
3078430874

30875+
function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) {
30876+
return checkTruthinessOfType(checkExpression(node, checkMode), node);
30877+
}
30878+
3078530879
function checkForStatement(node: ForStatement) {
3078630880
// Grammar checking
3078730881
if (!checkGrammarStatementInAmbientContext(node)) {

0 commit comments

Comments
 (0)