Caret topic (pipe operator) (#13749)

* 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>
This commit is contained in:
J. S. Choi
2021-10-28 16:04:55 -04:00
committed by GitHub
parent ddc45a5a50
commit ad59a2c618
402 changed files with 7200 additions and 128 deletions

View File

@@ -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 =.
case tt.moduloAssign: {
return this.parseTopicReferenceThenEqualsSign(tt.modulo, "%");
}
this.state.value = "%";
this.state.type = tt.modulo;
this.state.pos--;
this.state.end--;
this.state.endLoc.column--;
} else {
throw this.unexpected();
}
case tt.xorAssign: {
return this.parseTopicReferenceThenEqualsSign(tt.bitwiseXOR, "^");
}
// falls through
case tt.bitwiseXOR:
case tt.modulo:
case tt.hash: {
const pipeProposal = this.getPluginOption(
@@ -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 tokens type.
node = this.startNode();
const start = this.state.start;
const tokenType = this.state.type;
// Consume the current token.
this.next();
// If the pipe-operator plugins configuration matches the current tokens 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);
}
}
@@ -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 tokens 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 plugins configuration matches the current tokens 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>.

View File

@@ -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) {

View File

@@ -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;
}
@@ -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);
}
}

View File

@@ -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