Skip to content

Commit

Permalink
parser: Add caret as topic reference (implement)
Browse files Browse the repository at this point in the history
  • Loading branch information
js-choi committed Sep 11, 2021
1 parent b292bfd commit 4753d2c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 60 deletions.
18 changes: 8 additions & 10 deletions packages/babel-generator/src/generators/types.ts
Expand Up @@ -240,16 +240,14 @@ export function DecimalLiteral(this: Printer, node: t.DecimalLiteral) {
// Hack pipe operator
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);
}
const validTopicTokenSet = new Set(["^", "%", "#"]);

if (validTopicTokenSet.has(topicToken)) {
this.token(topicToken);
} else {
const givenTopicTokenJSON = JSON.stringify(topicToken);
const message = `The "topicToken" generator option must be "#" (${givenTopicTokenJSON} received instead).`;
throw new Error(message);
}
}

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
110 changes: 71 additions & 39 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -1232,26 +1232,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 @@ -1260,28 +1249,12 @@ 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);
} else {
throw this.unexpected();
}
}

// fall through
case tt.relational: {
if (this.state.value === "<") {
const lookaheadCh = this.input.codePointAt(this.nextTokenStart());
Expand All @@ -1290,16 +1263,75 @@ export default class ExpressionParser extends LValParser {
lookaheadCh === charCodes.greaterThan // Fragment <>
) {
this.expectOnePlugin(["jsx", "flow", "typescript"]);
break;
} else {
throw this.unexpected();
}
} else {
throw this.unexpected();
}
}

// fall through
default:
throw this.unexpected();
}
}

// 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 @@ -594,20 +594,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 @@ -681,11 +685,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
9 changes: 7 additions & 2 deletions packages/babel-parser/src/tokenizer/types.js
Expand Up @@ -140,9 +140,14 @@ export const types: { [name: string]: TokenType } = {
eq: new TokenType("=", { beforeExpr, isAssign }),
assign: new TokenType("_=", { beforeExpr, isAssign }),
slashAssign: new TokenType("_=", { 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.
// `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.
moduloAssign: new TokenType("_=", { beforeExpr, isAssign }),
// `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.
xorAssign: new TokenType("_=", { beforeExpr, isAssign }),
incDec: new TokenType("++/--", { prefix, postfix, startsExpr }),
bang: new TokenType("!", { beforeExpr, prefix, startsExpr }),
tilde: new TokenType("~", { beforeExpr, prefix, startsExpr }),
Expand Down
@@ -1,7 +1,7 @@
import { declare } from "@babel/helper-plugin-utils";

const PIPELINE_PROPOSALS = ["minimal", "fsharp", "hack", "smart"];
const TOPIC_TOKENS = ["%", "#"];
const TOPIC_TOKENS = ["^", "%", "#"];
const documentationURL =
"https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator";

Expand Down

0 comments on commit 4753d2c

Please sign in to comment.