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

Caret topic (pipe operator) #13749

Merged
merged 6 commits into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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": "^" }]]
}