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 21cfacb commit 4cd9599
Show file tree
Hide file tree
Showing 21 changed files with 105 additions and 253 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

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

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 comments on commit 4cd9599

Please sign in to comment.