Skip to content

Commit

Permalink
Caret topic (pipe operator) (#13749)
Browse files Browse the repository at this point in the history
* parser: Add caret as topic reference (tests)

* parser: Add caret as topic reference (implement)

* generator: Avoid reconstructing validTopicTokenSet

* babel-parser: Remove redundant throws in expression.js

* Minimize diff

* Update error message

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
  • Loading branch information
js-choi and nicolo-ribaudo committed Oct 28, 2021
1 parent ddc45a5 commit ad59a2c
Show file tree
Hide file tree
Showing 402 changed files with 7,202 additions and 130 deletions.
21 changes: 11 additions & 10 deletions packages/babel-generator/src/generators/types.ts
Expand Up @@ -238,18 +238,19 @@ export function DecimalLiteral(this: Printer, node: t.DecimalLiteral) {
}

// Hack pipe operator
const validTopicTokenSet = new Set(["^", "%", "#"]);
export function TopicReference(this: Printer) {
const { topicToken } = this.format;
switch (topicToken) {
case "#":
this.token("#");
break;

default: {
const givenTopicTokenJSON = JSON.stringify(topicToken);
const message = `The "topicToken" generator option must be "#" (${givenTopicTokenJSON} received instead).`;
throw new Error(message);
}

if (validTopicTokenSet.has(topicToken)) {
this.token(topicToken);
} else {
const givenTopicTokenJSON = JSON.stringify(topicToken);
const validTopics = Array.from(validTopicTokenSet, v => JSON.stringify(v));
throw new Error(
`The "topicToken" generator option must be one of ` +
`${validTopics.join(", ")} (${givenTopicTokenJSON} received instead).`,
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/babel-generator/src/index.ts
Expand Up @@ -203,7 +203,7 @@ export interface GeneratorOptions {
* For use with the Hack-style pipe operator.
* Changes what token is used for pipe bodies’ topic references.
*/
topicToken?: "#";
topicToken?: "^" | "%" | "#";
}

export interface GeneratorResult {
Expand Down
@@ -0,0 +1 @@
2 + 3 |> ^.toString(16);
@@ -0,0 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "^" }]],
"topicToken": "^"
}
@@ -0,0 +1 @@
2 + 3 |> ^.toString(16);
@@ -1,5 +1,5 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "#" }]],
"topicToken": "invalid",
"throws": "The \"topicToken\" generator option must be \"#\" (\"invalid\" received instead)."
"throws": "The \"topicToken\" generator option must be one of \"^\", \"%\", \"#\" (\"invalid\" received instead)."
}
@@ -1,4 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "#" }]],
"throws": "The \"topicToken\" generator option must be \"#\" (undefined received instead)."
"throws": "The \"topicToken\" generator option must be one of \"^\", \"%\", \"#\" (undefined received instead)."
}
@@ -0,0 +1 @@
2 + 3 |> %.toString(16);
@@ -0,0 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "%" }]],
"topicToken": "%"
}
@@ -0,0 +1 @@
2 + 3 |> %.toString(16);
101 changes: 64 additions & 37 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -1193,26 +1193,15 @@ export default class ExpressionParser extends LValParser {
return this.parsePrivateName();
}

case tt.moduloAssign:
if (
this.getPluginOption("pipelineOperator", "proposal") === "hack" &&
this.getPluginOption("pipelineOperator", "topicToken") === "%"
) {
// If we find %= in an expression position, and the Hack-pipes proposal is active,
// then the % could be the topic token (e.g., in x |> %==y or x |> %===y), and so we
// reparse it as %.
// The next readToken() call will start parsing from =.

this.state.value = "%";
this.state.type = tt.modulo;
this.state.pos--;
this.state.end--;
this.state.endLoc.column--;
} else {
throw this.unexpected();
}
case tt.moduloAssign: {
return this.parseTopicReferenceThenEqualsSign(tt.modulo, "%");
}

case tt.xorAssign: {
return this.parseTopicReferenceThenEqualsSign(tt.bitwiseXOR, "^");
}

// falls through
case tt.bitwiseXOR:
case tt.modulo:
case tt.hash: {
const pipeProposal = this.getPluginOption(
Expand All @@ -1221,24 +1210,7 @@ export default class ExpressionParser extends LValParser {
);

if (pipeProposal) {
// A pipe-operator proposal is active,
// although its configuration might not match the current token’s type.
node = this.startNode();
const start = this.state.start;
const tokenType = this.state.type;

// Consume the current token.
this.next();

// If the pipe-operator plugin’s configuration matches the current token’s type,
// then this will return `node`, will have been finished as a topic reference.
// Otherwise, this will throw a `PipeTopicUnconfiguredToken` error.
return this.finishTopicReference(
node,
start,
pipeProposal,
tokenType,
);
return this.parseTopicReference(pipeProposal);
}
}

Expand Down Expand Up @@ -1325,6 +1297,61 @@ export default class ExpressionParser extends LValParser {
}
}

// This helper method should only be called
// when the parser has reached a potential Hack pipe topic token
// that is followed by an equals sign.
// See <https://github.com/js-choi/proposal-hack-pipes>.
// If we find ^= or %= in an expression position
// (i.e., the tt.moduloAssign or tt.xorAssign token types),
// and if the Hack-pipes proposal is active with ^ or % as its topicToken,
// then the ^ or % could be the topic token (e.g., in x |> ^==y or x |> ^===y),
// and so we reparse the current token as ^ or %.
// Otherwise, this throws an unexpected-token error.
parseTopicReferenceThenEqualsSign(
topicTokenType: TokenType,
topicTokenValue: string,
): N.Expression {
const pipeProposal = this.getPluginOption("pipelineOperator", "proposal");

if (pipeProposal) {
// Set the most-recent token to be a topic token
// given by the tokenType and tokenValue.
// Now the next readToken() call (in parseTopicReference)
// will consume that “topic token”.
this.state.type = topicTokenType;
this.state.value = topicTokenValue;
// Rewind the tokenizer to the end of the “topic token”,
// so that the following token starts at the equals sign after that topic token.
this.state.pos--;
this.state.end--;
this.state.endLoc.column--;
// Now actually consume the topic token.
return this.parseTopicReference(pipeProposal);
} else {
throw this.unexpected();
}
}

// This helper method should only be called
// when the proposal-pipeline-operator plugin is active,
// and when the parser has reached a potential Hack pipe topic token.
// Although a pipe-operator proposal is assumed to be active,
// its configuration might not match the current token’s type.
// See <https://github.com/js-choi/proposal-hack-pipes>.
parseTopicReference(pipeProposal: string): N.Expression {
const node = this.startNode();
const start = this.state.start;
const tokenType = this.state.type;

// Consume the current token.
this.next();

// If the pipe-operator plugin’s configuration matches the current token’s type,
// then this will return `node`, will have been finished as a topic reference.
// Otherwise, this will throw a `PipeTopicUnconfiguredToken` error.
return this.finishTopicReference(node, start, pipeProposal, tokenType);
}

// This helper method attempts to finish the given `node`
// into a topic-reference node for the given `pipeProposal`.
// See <https://github.com/js-choi/proposal-hack-pipes>.
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-parser/src/plugin-utils.js
Expand Up @@ -39,7 +39,7 @@ export function getPluginOption(
}

const PIPELINE_PROPOSALS = ["minimal", "fsharp", "hack", "smart"];
const TOPIC_TOKENS = ["%", "#"];
const TOPIC_TOKENS = ["^", "%", "#"];
const RECORD_AND_TUPLE_SYNTAX_TYPES = ["hash", "bar"];

export function validatePlugins(plugins: PluginList) {
Expand Down
22 changes: 16 additions & 6 deletions packages/babel-parser/src/tokenizer/index.js
Expand Up @@ -605,20 +605,24 @@ export default class Tokenizer extends ParserErrors {
}

readToken_mult_modulo(code: number): void {
// '%*'
// '%' or '*'
let type = code === charCodes.asterisk ? tt.star : tt.modulo;
let width = 1;
let next = this.input.charCodeAt(this.state.pos + 1);

// Exponentiation operator **
// Exponentiation operator '**'
if (code === charCodes.asterisk && next === charCodes.asterisk) {
width++;
next = this.input.charCodeAt(this.state.pos + 2);
type = tt.exponent;
}

// '%=' or '*='
if (next === charCodes.equalsTo && !this.state.inType) {
width++;
// `tt.moduloAssign` is only needed to support % as a Hack-pipe topic token.
// If the proposal ends up choosing a different token,
// it can be merged with tt.assign.
type = code === charCodes.percentSign ? tt.moduloAssign : tt.assign;
}

Expand Down Expand Up @@ -692,11 +696,17 @@ export default class Tokenizer extends ParserErrors {
}

readToken_caret(): void {
// '^'
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === charCodes.equalsTo) {
this.finishOp(tt.assign, 2);
} else {

// '^='
if (next === charCodes.equalsTo && !this.state.inType) {
// `tt.xorAssign` is only needed to support ^ as a Hack-pipe topic token.
// If the proposal ends up choosing a different token,
// it can be merged with tt.assign.
this.finishOp(tt.xorAssign, 2);
}
// '^'
else {
this.finishOp(tt.bitwiseXOR, 1);
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/babel-parser/src/tokenizer/types.js
Expand Up @@ -182,8 +182,9 @@ export const tt: { [name: string]: TokenType } = {
eq: createToken("=", { beforeExpr, isAssign }),
assign: createToken("_=", { beforeExpr, isAssign }),
slashAssign: createToken("_=", { beforeExpr, isAssign }),
// This is only needed to support % as a Hack-pipe topic token. If the proposal
// ends up choosing a different token, it can be merged with tt.assign.
// These are only needed to support % and ^ as a Hack-pipe topic token. When the
// proposal settles on a token, the others can be merged with tt.assign.
xorAssign: createToken("_=", { beforeExpr, isAssign }),
moduloAssign: createToken("_=", { beforeExpr, isAssign }),
// end: isAssign

Expand Down
@@ -0,0 +1 @@
value |> ^ + 1
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "^" }]]
}
@@ -0,0 +1,45 @@
{
"type": "File",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"program": {
"type": "Program",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"expression": {
"type": "BinaryExpression",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"left": {
"type": "Identifier",
"start":0,"end":5,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":5},"identifierName":"value"},
"name": "value"
},
"operator": "|>",
"right": {
"type": "BinaryExpression",
"start":9,"end":14,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":14}},
"left": {
"type": "TopicReference",
"start":9,"end":10,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":10}}
},
"operator": "+",
"right": {
"type": "NumericLiteral",
"start":13,"end":14,"loc":{"start":{"line":1,"column":13},"end":{"line":1,"column":14}},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
value |> 1 + ^
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "^" }]]
}
@@ -0,0 +1,45 @@
{
"type": "File",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"program": {
"type": "Program",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"expression": {
"type": "BinaryExpression",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":14}},
"left": {
"type": "Identifier",
"start":0,"end":5,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":5},"identifierName":"value"},
"name": "value"
},
"operator": "|>",
"right": {
"type": "BinaryExpression",
"start":9,"end":14,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":14}},
"left": {
"type": "NumericLiteral",
"start":9,"end":10,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":10}},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
},
"operator": "+",
"right": {
"type": "TopicReference",
"start":13,"end":14,"loc":{"start":{"line":1,"column":13},"end":{"line":1,"column":14}}
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
value |> a + b
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "^" }]]
}

0 comments on commit ad59a2c

Please sign in to comment.