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

WIP: parse pattern matching #13572

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/babel-parser/src/parser/error-message.js
Expand Up @@ -77,6 +77,8 @@ export const ErrorMessages = makeErrorTemplates(
ImportCallNotNewExpression: "Cannot use new with import(...).",
ImportCallSpreadArgument: "`...` is not allowed in `import()`.",
InvalidBigIntLiteral: "Invalid BigIntLiteral.",
InvalidBinaryMatchPattern:
"Different binary match pattern operators cannot be at the same level.",
InvalidCodePoint: "Code point out of bounds.",
InvalidDecimal: "Invalid decimal.",
InvalidDigit: "Expected number in radix %0.",
Expand All @@ -99,6 +101,8 @@ export const ErrorMessages = makeErrorTemplates(
LabelRedeclaration: "Label '%0' is already declared.",
LetInLexicalBinding:
"'let' is not allowed to be used as a name in 'let' or 'const' declarations.",
LineTerminatorAfterMatchPattern:
"No line terminator allowed after match pattern.",
LineTerminatorBeforeArrow: "No line break is allowed before '=>'.",
MalformedRegExpFlags: "Invalid regular expression flag.",
MissingClassName: "A class name is required.",
Expand Down Expand Up @@ -175,6 +179,8 @@ export const ErrorMessages = makeErrorTemplates(
RestTrailingComma: "Unexpected trailing comma after rest element.",
SloppyFunction:
"In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.",
SpreadElementInMatchExpression:
"Spread elements are not allowed in match expressions.",
StaticPrototype: "Classes may not have static property named prototype.",
StrictDelete: "Deleting local variable in strict mode.",
StrictEvalArguments: "Assigning to '%0' in strict mode.",
Expand Down
248 changes: 248 additions & 0 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -93,6 +93,8 @@ export default class ExpressionParser extends LValParser {
+parseProgram: (
program: N.Program, end: TokenType, sourceType?: SourceType
) => N.Program
+parseStatement: (context: ?string, topLevel?: boolean) => N.Statement
+parseHeaderExpression: () => N.Expression
*/

// For object literal, check if property __proto__ has been used more than once.
Expand Down Expand Up @@ -813,6 +815,7 @@ export default class ExpressionParser extends LValParser {
optional: boolean,
): N.Expression {
const oldMaybeInArrowParameters = this.state.maybeInArrowParameters;
const lineTerminatorAfterId = this.isLineTerminator();
let refExpressionErrors = null;

this.state.maybeInArrowParameters = true;
Expand Down Expand Up @@ -851,6 +854,18 @@ export default class ExpressionParser extends LValParser {
this.startNodeAt(startPos, startLoc),
node,
);
} else if (
base.name === "match" &&
this.hasPlugin("patternMatching") &&
!lineTerminatorAfterId &&
!this.isLineTerminator() &&
!optional &&
node.arguments.length > 0
) {
node = this.parseMatchExpressionFromCallExpression(
this.startNodeAt<N.MatchExpression>(startPos, startLoc),
node,
);
} else {
if (state.maybeAsyncArrow) {
this.checkExpressionErrors(refExpressionErrors, true);
Expand Down Expand Up @@ -2934,4 +2949,237 @@ export default class ExpressionParser extends LValParser {
this.eat(tt.braceR);
return this.finishNode<N.ModuleExpression>(node, "ModuleExpression");
}

// https://github.com/tc39/proposal-pattern-matching
parseMatchExpressionFromCallExpression(
node: N.MatchExpression,
callExpression: N.CallExpression,
): N.MatchExpression {
const expressions = callExpression.arguments;
this.checkMatchExpressionDiscriminant(expressions);
if (expressions.length > 1) {
const sequence = this.startNodeAtNode(expressions[0]);
sequence.expressions = expressions;
const lastExpression = expressions[expressions.length - 1];
node.discriminant = this.finishNodeAt(
sequence,
"SequenceExpression",
lastExpression.end,
lastExpression.loc.end,
);
} else {
node.discriminant = expressions[0];
}
node.clauses = this.parseMatchClauses();
return this.finishNode(node, "MatchExpression");
}

checkMatchExpressionDiscriminant(
expressions: Array<N.Expression | N.SpreadElement>,
) {
for (const expression of expressions) {
if (expression.type === "SpreadElement") {
this.unexpected(
expression.start,
Errors.SpreadElementInMatchExpression,
);
}
}
}

parseMatchClauses(): N.MatchClause[] {
this.eat(tt.braceL);
const clauses = [];
while (!this.eat(tt.braceR)) {
if (this.match(tt._if) || this.match(tt._else)) {
clauses.push(this.parseConditionalMatchClause());
} else if (this.isContextual("when")) {
clauses.push(this.parseWhenMatchClause());
} else {
this.unexpected();
}
}
return clauses;
}

parseConditionalMatchClause(): N.MatchClause {
const node = this.startNode<N.MatchClause>();
const type = this.state.type;
this.next(); // skip "if" / "else"
if (type === tt._if) {
node.guard = this.parseHeaderExpression();
}
node.consequent = this.parseStatement("if");
return this.finishNode(node, "MatchClause");
}

parseWhenMatchClause(): N.MatchClause {
const node = this.startNode<N.MatchClause>();
this.next(); // skip "when"
if (this.match(tt.bitwiseXOR)) {
node.test = this.parseExpressionMatchPattern();
} else {
this.expect(tt.parenL);
node.test = this.parseMaybeBinaryMatchPattern();
this.expect(tt.parenR);
}

if (this.isLineTerminator()) {
this.unexpected(
this.state.lastTokEnd,
Errors.LineTerminatorAfterMatchPattern,
);
}
node.consequent = this.parseStatement("do");
fedeci marked this conversation as resolved.
Show resolved Hide resolved
return this.finishNode(node, "MatchClause");
}

parseMatchPattern(): N.MatchPattern {
switch (this.state.type) {
case tt.slash:
case tt.slashAssign:
this.readRegexp();
return this.parseRegExpLiteral(this.state.value);
case tt.num:
return this.parseNumericLiteral(this.state.value);
case tt.bigint:
return this.parseBigIntLiteral(this.state.value);
case tt.string:
return this.parseStringLiteral(this.state.value);
case tt._null:
return this.parseNullLiteral();
case tt._true:
return this.parseBooleanLiteral(true);
case tt._false:
return this.parseBooleanLiteral(false);
case tt.name:
return this.parseIdentifier();
case tt.braceL:
return this.parseObjectMatchPattern();
case tt.bracketL:
return this.parseArrayMatchPattern();
case tt.plusMin:
return this.parseSimpleUnaryExpression();
case tt.bitwiseXOR:
return this.parseExpressionMatchPattern();
default:
throw this.unexpected();
}
}

parseExpressionMatchPattern(): N.ExpressionMatchPattern {
const node = this.startNode<N.ExpressionMatchPattern>();
this.next(); // skip ^
let expression: N.Expression;
if (this.match(tt.parenL)) {
expression = this.parseParenAndDistinguishExpression(false);
} else {
expression = this.parseExprSubscripts();
}

node.expression = expression;
return this.finishNode(node, "ExpressionMatchPattern");
}

parseSimpleUnaryExpression(): N.UnaryExpression {
const node = this.startNode<N.UnaryExpression>();
node.operator = this.state.value;
this.next(); // skip operator (+/-)
node.prefix = true;
switch (this.state.type) {
case tt.num:
node.argument = this.parseNumericLiteral(this.state.value);
break;
case tt.bigint:
node.argument = this.parseBigIntLiteral(this.state.value);
break;
default:
if (this.isContextual("Infinity")) {
node.argument = this.parseIdentifier();
break;
}
throw this.unexpected();
}
return this.finishNode(node, "UnaryExpression");
}

parseMaybeBinaryMatchPattern(
previousOp?: "and" | "or",
): N.BinaryMatchPattern | N.MatchPattern {
const node = this.startNode<N.BinaryMatchPattern>();
const lhs = this.parseMatchPattern();
if (this.match(tt.bitwiseOR) || this.match(tt.bitwiseAND)) {
const operator = this.match(tt.bitwiseOR) ? "or" : "and";
if (previousOp && previousOp !== operator) {
this.unexpected(this.state.start, Errors.InvalidBinaryMatchPattern);
}
this.next(); // skip "or" or "and"
node.left = lhs;
node.operator = operator;
node.right = this.parseMaybeBinaryMatchPattern(operator);
return this.finishNode(node, "BinaryMatchPattern");
} else {
return lhs;
}
}

parseObjectMatchPattern(): N.ObjectMatchPattern {
const node = this.startNode<N.ObjectMatchPattern>();
this.expect(tt.braceL);
const properties: (N.AssignmentMatchProperty | N.RestMatchElement)[] = [];
while (!this.eat(tt.braceR)) {
if (this.match(tt.ellipsis)) {
properties.push(this.parseRestMatchElement());
this.checkCommaAfterRest(charCodes.rightCurlyBrace);
this.expect(tt.braceR);
break;
}
const node = this.startNode<N.AssignmentMatchProperty>();
node.method = false;
this.parsePropertyName(node, /* isPrivateNameAllowed */ false);
if (this.eat(tt.colon)) {
fedeci marked this conversation as resolved.
Show resolved Hide resolved
node.value = this.parseMaybeBinaryMatchPattern();
fedeci marked this conversation as resolved.
Show resolved Hide resolved
}
this.finishNode(node, "ObjectProperty");
properties.push(node);
this.eat(tt.comma);
}

node.properties = properties;
return this.finishNode(node, "ObjectMatchPattern");
}

parseArrayMatchPattern(): N.ArrayMatchPattern {
const node = this.startNode<N.ArrayMatchPattern>();
this.expect(tt.bracketL);

const elements: N.MatchPattern[] = [];

while (!this.eat(tt.bracketR)) {
if (this.match(tt.ellipsis)) {
elements.push(this.parseRestMatchElement());
} else if (this.isContextual("_")) {
elements.push(this.parseNullMatchPattern());
} else {
elements.push(this.parseMaybeBinaryMatchPattern());
}
this.eat(tt.comma);
}

node.elements = elements;
return this.finishNode(node, "ArrayMatchPattern");
}

parseNullMatchPattern(): N.NullMatchPattern {
const node = this.startNode();
this.next(); // skip "_"
return this.finishNode(node, "NullMatchPattern");
}

parseRestMatchElement(): N.RestMatchElement {
const node = this.startNode();
this.expect(tt.ellipsis);
node.argument = this.parseMaybeBinaryMatchPattern();
return this.finishNode(node, "RestMatchElement");
}
}
70 changes: 70 additions & 0 deletions packages/babel-parser/src/types.js
Expand Up @@ -283,6 +283,76 @@ export type SwitchCase = NodeBase & {
consequent: $ReadOnlyArray<Statement>,
};

// Pattern Matching
export type MatchExpression = Expression & {
type: "MatchExpression",
discriminant: Expression,
id: Pattern | null,
clauses: $ReadOnlyArray<MatchClause>,
};

export type MatchClause = NodeBase & {
type: "MatchClause",
test: MatchPattern | null,
guard: Expression | null,
id: Pattern | null,
consequent: BlockStatement,
};

export type MatchPattern =
| ArrayMatchPattern
| ObjectMatchPattern
| RestMatchElement
| BinaryMatchPattern
| AsMatchPattern
| ExpressionMatchPattern
| NullMatchPattern
| Literal
| Identifier
| UnaryExpression;

export type ArrayMatchPattern = NodeBase & {
type: "ArrayMatchPattern",
elements: $ReadOnlyArray<MatchPattern>,
};

export type AssignmentMatchProperty = ObjectProperty & {
value: MatchPattern,
kind: "init",
method: false,
};

export type ObjectMatchPattern = NodeBase & {
type: "ObjectMatchPattern",
properties: $ReadOnlyArray<AssignmentMatchProperty | RestMatchElement>,
};

export type RestMatchElement = NodeBase & {
type: "RestMatchElement",
argument: MatchPattern,
};

export type BinaryMatchPattern = NodeBase & {
type: "BinaryMatchPattern",
operator: "and" | "or" | "with",
left: MatchPattern,
right: MatchPattern,
};

export type AsMatchPattern = NodeBase & {
type: "AsMatchPattern",
test: MatchPattern,
id: Pattern,
};

export type ExpressionMatchPattern = NodeBase & {
type: "ExpressionMatchPattern",
expression: Expression,
};

export type NullMatchPattern = NodeBase & {
type: "NullMatchPattern",
};
// Exceptions

export type ThrowStatement = NodeBase & {
Expand Down
@@ -0,0 +1,7 @@
match(x) {
when([
_,
_,
{ foo: 200 },
]) { "foo" }
}