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

Add hack style pipeline proposal #11600

Closed
wants to merge 10 commits into from
  •  
  •  
  •  
16 changes: 1 addition & 15 deletions packages/babel-generator/src/generators/types.ts
Expand Up @@ -236,20 +236,6 @@ export function DecimalLiteral(this: Printer, node: t.DecimalLiteral) {
this.word(node.value + "m");
}

export function PipelineTopicExpression(
this: Printer,
node: t.PipelineTopicExpression,
) {
this.print(node.expression, node);
}

export function PipelineBareFunction(
this: Printer,
node: t.PipelineBareFunction,
) {
this.print(node.callee, node);
}

export function PipelinePrimaryTopicReference(this: Printer) {
export function TopicReference(this: Printer) {
this.token("#");
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

@@ -0,0 +1 @@
2 + 3 |> #.toString(16);
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack" }]]
}
@@ -0,0 +1 @@
2 + 3 |> #.toString(16);
43 changes: 4 additions & 39 deletions packages/babel-parser/ast/spec.md
Expand Up @@ -962,48 +962,13 @@ If `object` is `null`, then `callee` should be a `MemberExpression`.

### Pipeline

These nodes are used by the Smart Pipeline to determine the type of the expression in a Pipeline Operator Expression. The F# Pipeline uses simple `BinaryExpression`s.
These nodes are used by the Hack-style pipe operator. (The F# Pipeline uses simple `BinaryExpression`s and `AwaitExpression`s.)

#### PipelineBody
#### PipeBody

```js
interface PipelineBody <: NodeBase {
type: "PipelineBody";
}
```

#### PipelineBareFunctionBody

```js
interface PipelineBody <: NodeBase {
type: "PipelineBareFunctionBody";
callee: Expression;
}
```

#### PipelineBareConstructorBody

```js
interface PipelineBareConstructorBody <: NodeBase {
type: "PipelineBareConstructorBody";
callee: Expression;
}
```

#### PipelineBareAwaitedFunctionBody

```js
interface PipelineBareConstructorBody <: NodeBase {
type: "PipelineTopicBody";
expression: Expression;
}
```

#### PipelineTopicBody

```js
interface PipelineBareConstructorBody <: NodeBase {
type: "PipelineBareAwaitedFunctionBody";
interface PipeBody <: NodeBase {
type: "PipeBody";
callee: Expression;
}
```
Expand Down
20 changes: 8 additions & 12 deletions packages/babel-parser/src/parser/error-message.js
Expand Up @@ -129,18 +129,14 @@ export const ErrorMessages = Object.freeze({
ParamDupe: "Argument name clash",
PatternHasAccessor: "Object pattern can't contain getter or setter",
PatternHasMethod: "Object pattern can't contain methods",
PipelineBodyNoArrow:
'Unexpected arrow "=>" after pipeline body; arrow function in pipeline body must be parenthesized',
PipelineBodySequenceExpression:
"Pipeline body may not be a comma-separated sequence expression",
PipelineHeadSequenceExpression:
"Pipeline head should not be a comma-separated sequence expression",
PipelineTopicUnused:
"Pipeline is in topic style but does not use topic reference",
PrimaryTopicNotAllowed:
"Topic reference was used in a lexical context without topic binding",
PrimaryTopicRequiresSmartPipeline:
"Primary Topic Reference found but pipelineOperator not passed 'smart' for 'proposal' option.",
PipeBodyCannotBeArrow:
'Unexpected arrow "=>" after pipeline body; arrow function acting as pipe body must be parenthesized due to operator precedence',
JLHwung marked this conversation as resolved.
Show resolved Hide resolved
PipeTopicRequiresHackPipes:
"Topic reference is used, but the pipelineOperator plugin was not passed a 'proposal': 'hack' option.",
PipeTopicUnbound:
"Topic reference is unbound; it must be inside a Hack-style pipe body",
PipeTopicUnused:
"Hack-style pipe body does not contain a topic reference; Hack-style pipes must use topic at least once",
PrivateInExpectedIn:
"Private names are only allowed in property accesses (`obj.#%0`) or in `in` expressions (`#%0 in obj`)",
PrivateNameRedeclaration: "Duplicate private name #%0",
Expand Down
147 changes: 32 additions & 115 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -394,7 +394,6 @@ export default class ExpressionParser extends LValParser {
return left;
}
this.state.inPipeline = true;
this.checkPipelineAtInfixOperator(left, leftStartPos);
}
const node = this.startNodeAt(leftStartPos, leftStartLoc);
node.left = left;
Expand Down Expand Up @@ -456,24 +455,21 @@ export default class ExpressionParser extends LValParser {

parseExprOpRightExpr(op: TokenType, prec: number): N.Expression {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
switch (op) {
case tt.pipeline:
switch (this.getPluginOption("pipelineOperator", "proposal")) {
case "smart":
return this.withTopicPermittingContext(() => {
return this.parseSmartPipelineBody(
this.parseExprOpBaseRightExpr(op, prec),
startPos,
startLoc,
);
case "hack":
return this.withTopicBindingContext(() => {
const bodyExpr = this.parseExprOpBaseRightExpr(op, prec);
this.checkHackPipeBodyEarlyErrors(startPos);
return bodyExpr;
});
case "fsharp":
return this.withSoloAwaitPermittingContext(() => {
return this.parseFSharpPipelineBody(prec);
});
}
// falls through
// Falls through.

default:
return this.parseExprOpBaseRightExpr(op, prec);
Expand Down Expand Up @@ -1153,20 +1149,18 @@ export default class ExpressionParser extends LValParser {
if (this.state.inPipeline) {
node = this.startNode();

if (
this.getPluginOption("pipelineOperator", "proposal") !== "smart"
) {
this.raise(node.start, Errors.PrimaryTopicRequiresSmartPipeline);
if (this.getPluginOption("pipelineOperator", "proposal") !== "hack") {
this.raise(node.start, Errors.PipeTopicRequiresHackPipes);
}

this.next();

if (!this.primaryTopicReferenceIsAllowedInCurrentTopicContext()) {
this.raise(node.start, Errors.PrimaryTopicNotAllowed);
if (!this.topicReferenceIsAllowedInCurrentContext()) {
this.raise(node.start, Errors.PipeTopicUnbound);
}

this.registerTopicReference();
return this.finishNode(node, "PipelinePrimaryTopicReference");
return this.finishNode(node, "TopicReference");
}

// https://tc39.es/proposal-private-fields-in-in
Expand Down Expand Up @@ -2466,87 +2460,33 @@ 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) {
if (this.getPluginOption("pipelineOperator", "proposal") === "smart") {
if (left.type === "SequenceExpression") {
// Ensure that the pipeline head is not a comma-delimited
// sequence expression.
this.raise(leftStartPos, Errors.PipelineHeadSequenceExpression);
}
}
}

parseSmartPipelineBody(
childExpression: N.Expression,
startPos: number,
startLoc: Position,
): N.PipelineBody {
this.checkSmartPipelineBodyEarlyErrors(childExpression, startPos);
// This helper method is to be called immediately
// after a Hack-style pipe body is parsed.
// The `startPos` is the starting position of the pipe body.

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

checkSmartPipelineBodyEarlyErrors(
childExpression: N.Expression,
startPos: number,
): void {
checkHackPipeBodyEarlyErrors(startPos: number): void {
// If the following token is invalidly `=>`, then throw a human-friendly error
// instead of something like 'Unexpected token, expected ";"'.
// For example, `x => x |> y => #` (assuming `#` is the topic reference)
// groups into `x => (x |> y) => #`,
// and `(x |> y) => #` is an invalid arrow function.
// This is because Hack-style `|>` has tighter precedence than `=>`.
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, Errors.PipelineBodyNoArrow);
} else if (childExpression.type === "SequenceExpression") {
this.raise(startPos, Errors.PipelineBodySequenceExpression);
throw this.raise(this.state.start, Errors.PipeBodyCannotBeArrow);
}
}

parseSmartPipelineBodyInStyle(
childExpression: N.Expression,
startPos: number,
startLoc: Position,
): N.PipelineBody {
const bodyNode = this.startNodeAt(startPos, startLoc);
const isSimpleReference = this.isSimpleReference(childExpression);
if (isSimpleReference) {
bodyNode.callee = childExpression;
} else {
if (!this.topicReferenceWasUsedInCurrentTopicContext()) {
this.raise(startPos, Errors.PipelineTopicUnused);
}
bodyNode.expression = childExpression;
// A Hack pipe body must use the topic reference at least once.
else if (!this.topicReferenceWasUsedInCurrentContext()) {
this.raise(startPos, Errors.PipeTopicUnused);
}
return this.finishNode(
bodyNode,
isSimpleReference ? "PipelineBareFunction" : "PipelineTopicExpression",
);
}

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

// Enable topic references from outer contexts within smart pipeline bodies.
// Enable topic references from outer contexts within Hack-style pipe 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.
// the use of topic references.
// The function then calls a callback, then resets the parser
// to the old topic-context state that it had before the function was called.

withTopicPermittingContext<T>(callback: () => T): T {
withTopicBindingContext<T>(callback: () => T): T {
const outerContextTopicState = this.state.topicContext;
this.state.topicContext = {
// Enable the use of the primary topic reference.
Expand All @@ -2562,29 +2502,6 @@ export default class ExpressionParser extends LValParser {
}
}

// 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.state.topicContext;
this.state.topicContext = {
// Disable the use of the primary topic reference.
maxNumOfResolvableTopics: 0,
// Hide the use of any topic references from outer contexts.
maxTopicIndex: null,
};

try {
return callback();
} finally {
this.state.topicContext = outerContextTopicState;
}
}

withSoloAwaitPermittingContext<T>(callback: () => T): T {
const outerContextSoloAwaitState = this.state.soloAwait;
this.state.soloAwait = true;
Expand Down Expand Up @@ -2630,11 +2547,11 @@ export default class ExpressionParser extends LValParser {
this.state.topicContext.maxTopicIndex = 0;
}

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

topicReferenceWasUsedInCurrentTopicContext(): boolean {
topicReferenceWasUsedInCurrentContext(): boolean {
return (
this.state.topicContext.maxTopicIndex != null &&
this.state.topicContext.maxTopicIndex >= 0
Expand Down