Skip to content

Commit

Permalink
Implement Smart Pipeline proposal in @babel/parser
Browse files Browse the repository at this point in the history
  • Loading branch information
js-choi authored and mAAdhaTTah committed Jul 8, 2018
1 parent 4d125c3 commit 12da3b9
Show file tree
Hide file tree
Showing 223 changed files with 11,297 additions and 107 deletions.
312 changes: 298 additions & 14 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -24,6 +24,8 @@ import LValParser from "./lval";
import { reservedWords } from "../util/identifier";
import type { Pos, Position } from "../util/location";

const PIPELINE_PROPOSALS = ["minimal", "smart"];

export default class ExpressionParser extends LValParser {
// Forward-declaration: defined in statement.js
+parseBlock: (allowDirectives?: boolean) => N.BlockStatement;
Expand Down Expand Up @@ -296,18 +298,19 @@ export default class ExpressionParser extends LValParser {
}

const op = this.state.type;
if (op === tt.nullishCoalescing) {

if (op === tt.pipeline) {
this.checkPipelineAtInfixOperator(left, leftStartPos);
} else if (op === tt.nullishCoalescing) {
this.expectPlugin("nullishCoalescingOperator");
} else if (op === tt.pipeline) {
this.expectPlugin("pipelineOperator");
}

this.next();

const startPos = this.state.start;
const startLoc = this.state.startLoc;

if (op === tt.pipeline) {
if (
op === tt.pipeline &&
"minimal" === this.getPluginOption("pipelineOperator", "proposal")
) {
if (
this.match(tt.name) &&
this.state.value === "await" &&
Expand All @@ -320,13 +323,7 @@ export default class ExpressionParser extends LValParser {
}
}

node.right = this.parseExprOp(
this.parseMaybeUnary(),
startPos,
startLoc,
op.rightAssociative ? prec - 1 : prec,
noIn,
);
node.right = this.parseExprOpRightExpr(op, prec, noIn);

this.finishNode(
node,
Expand All @@ -336,6 +333,7 @@ export default class ExpressionParser extends LValParser {
? "LogicalExpression"
: "BinaryExpression",
);

return this.parseExprOp(
node,
leftStartPos,
Expand All @@ -348,6 +346,55 @@ export default class ExpressionParser extends LValParser {
return left;
}

// Helper function for `parseExprOp`. Parse the right-hand side of binary-
// operator expressions, then apply any operator-specific functions.

parseExprOpRightExpr(
op: TokenType,
prec: number,
noIn: ?boolean,
): N.Expression {
switch (op) {
case tt.pipeline:
if ("smart" === this.getPluginOption("pipelineOperator", "proposal")) {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
return this.withTopicPermittingContext(() => {
return this.parseSmartPipelineBody(
this.parseExprOpBaseRightExpr(op, prec, noIn),
startPos,
startLoc,
);
});
} else {
return this.parseExprOpBaseRightExpr(op, prec, noIn);
}

default:
return this.parseExprOpBaseRightExpr(op, prec, noIn);
}
}

// Helper function for `parseExprOpRightExpr`. Parse the right-hand side of
// binary-operator expressions without applying any operator-specific functions.

parseExprOpBaseRightExpr(
op: TokenType,
prec: number,
noIn: ?boolean,
): N.Expression {
const startPos = this.state.start;
const startLoc = this.state.startLoc;

return this.parseExprOp(
this.parseMaybeUnary(),
startPos,
startLoc,
op.rightAssociative ? prec - 1 : prec,
noIn,
);
}

// Parse unary operators, both prefix and postfix.

parseMaybeUnary(refShorthandDefaultPos: ?Pos): N.Expression {
Expand Down Expand Up @@ -886,6 +933,29 @@ export default class ExpressionParser extends LValParser {
}
}

case tt.primaryTopicReference: {
this.expectPlugin("pipelineOperator");
node = this.startNode();

if ("smart" !== this.getPluginOption("pipelineOperator", "proposal")) {
this.raise(
node.start,
"Primary Topic Reference found but pipelineOperator not passed 'smart' for 'proposal' option.",
);
}

this.next();
if (this.primaryTopicReferenceIsAllowedInCurrentTopicContext()) {
this.registerTopicReference();
return this.finishNode(node, "PrimaryTopicReference");
} else {
throw this.raise(
node.start,
`Topic reference was used in a lexical context without topic binding`,
);
}
}

default:
throw this.unexpected();
}
Expand Down Expand Up @@ -1921,4 +1991,218 @@ export default class ExpressionParser extends LValParser {
}
return this.finishNode(node, "YieldExpression");
}

// Validates a pipeline (for any of the pipeline Babylon plugins) at the point
// of the infix operator `|>`.

checkPipelineAtInfixOperator(left: N.Expression, leftStartPos: number) {
this.expectPlugin("pipelineOperator");

if (
!PIPELINE_PROPOSALS.includes(
this.getPluginOption("pipelineOperator", "proposal"),
)
) {
this.raise(
leftStartPos,
"'pipelineOperator' requires 'proposal' option whose value should be one of: " +
PIPELINE_PROPOSALS.join(", "),
);
}

if ("smart" === this.getPluginOption("pipelineOperator", "proposal")) {
this.checkSmartPipelineHeadEarlyErrors(left, leftStartPos);
}
}

checkSmartPipelineHeadEarlyErrors(left: N.Expression, leftStartPos: number) {
if (left.type === "SequenceExpression") {
// Ensure that the pipeline head is not a comma-delimited
// sequence expression.
throw this.raise(
leftStartPos,
`Pipeline head may not be a comma-separated sequence expression`,
);
}
}

parseSmartPipelineBody(
childExpression: N.Expression,
startPos: number,
startLoc: Position,
): N.PipelineBody {
const pipelineStyle = this.checkSmartPipelineBodyStyle(childExpression);

this.checkSmartPipelineBodyEarlyErrors(
childExpression,
pipelineStyle,
startPos,
);

return this.parseSmartPipelineBodyInStyle(
childExpression,
pipelineStyle,
startPos,
startLoc,
);
}

checkSmartPipelineBodyEarlyErrors(
childExpression: N.Expression,
pipelineStyle: PipelineStyle,
startPos: number,
): N.PipelineBody {
if (this.match(tt.arrow)) {
// If the following token is invalidly `=>`, then throw a human-friendly error
// instead of something like 'Unexpected token, expected ";"'.
throw this.raise(
this.state.start,
`Unexpected arrow "=>" after pipeline body; arrow function in pipeline body must be parenthesized`,
);
} else if (
pipelineStyle === "PipelineTopicExpression" &&
childExpression.type === "SequenceExpression"
) {
throw this.raise(
startPos,
`Pipeline body may not be a comma-separated sequence expression`,
);
}
}

parseSmartPipelineBodyInStyle(
childExpression: N.Expression,
pipelineStyle: PipelineStyle,
startPos: number,
startLoc: Position,
): N.PipelineBody {
const bodyNode = this.startNodeAt(startPos, startLoc);
switch (pipelineStyle) {
case "PipelineBareFunction":
bodyNode.callee = childExpression;
break;
case "PipelineBareConstructor":
bodyNode.callee = childExpression.callee;
break;
case "PipelineBareAwaitedFunction":
bodyNode.callee = childExpression.argument;
break;
case "PipelineTopicExpression":
if (!this.topicReferenceWasUsedInCurrentTopicContext()) {
throw this.raise(
startPos,
`Pipeline is in topic style but does not use topic reference`,
);
}
bodyNode.expression = childExpression;
break;
default:
throw this.raise(startPos, `Unknown pipeline style ${pipelineStyle}`);
}
return this.finishNode(bodyNode, pipelineStyle);
}

checkSmartPipelineBodyStyle(
expression: N.Expression,
):
| "PipelineBareFunction"
| "PipelineBareConstructor"
| "PipelineBareAwaitedFunction"
| "PipelineTopicExpression" {
switch (expression.type) {
case "NewExpression":
return this.isSimpleReference(expression.callee)
? "PipelineBareConstructor"
: "PipelineTopicExpression";
case "AwaitExpression":
return this.isSimpleReference(expression.argument)
? "PipelineBareAwaitedFunction"
: "PipelineTopicExpression";
default:
return this.isSimpleReference(expression)
? "PipelineBareFunction"
: "PipelineTopicExpression";
}
}

isSimpleReference(expression: N.Expression): boolean {
switch (expression.type) {
case "MemberExpression":
return this.isSimpleReference(expression.object);
case "Identifier":
return true;
default:
return false;
}
}

// Enable topic references from outer contexts within smart pipeline bodies.
// The function modifies the parser's topic-context state to enable or disable
// the use of topic references with the smartPipelines plugin. They then run a
// callback, then they reset the parser to the old topic-context state that it
// had before the function was called.

withTopicPermittingContext<T>(callback: () => T): T {
const outerContextTopicState = this.readTopicContextState();
this.state.topicContextState = {
// Enable the use of the primary topic reference.
maxNumOfResolvableTopics: 1,
// Hide the use of any topic references from outer contexts.
maxTopicIndex: null,
};

const callbackResult = callback();

this.state.topicContextState = outerContextTopicState;
return callbackResult;
}

// Disable topic references from outer contexts within syntax constructs
// such as the bodies of iteration statements.
// The function modifies the parser's topic-context state to enable or disable
// the use of topic references with the smartPipelines plugin. They then run a
// callback, then they reset the parser to the old topic-context state that it
// had before the function was called.

withTopicForbiddingContext<T>(callback: () => T): T {
const outerContextTopicState = this.readTopicContextState();
this.state.topicContextState = {
// Disable the use of the primary topic reference.
maxNumOfResolvableTopics: 0,
// Hide the use of any topic references from outer contexts.
maxTopicIndex: null,
};

const callbackResult = callback();

this.state.topicContextState = outerContextTopicState;
return callbackResult;
}

readTopicContextState(): tt.TopicContextState {
return this.state.topicContextState;
}

// Register the use of a primary topic reference (`#`) within the current
// topic context.
registerTopicReference(): void {
this.state.topicContextState.maxTopicIndex = 0;
}

primaryTopicReferenceIsAllowedInCurrentTopicContext(): boolean {
return this.state.topicContextState.maxNumOfResolvableTopics >= 1;
}

topicReferenceWasUsedInCurrentTopicContext(): boolean {
return (
this.state.topicContextState.maxTopicIndex != null &&
this.state.topicContextState.maxTopicIndex >= 0
);
}
}

type PipelineStyle =
| "PipelineBareFunction"
| "PipelineBareConstructor"
| "PipelineBareAwaitedFunction"
| "PipelineTopicExpression";

0 comments on commit 12da3b9

Please sign in to comment.