Skip to content

Commit 801bae6

Browse files
committed
Pattern Matching
1 parent 7fcfc6a commit 801bae6

File tree

68 files changed

+9608
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+9608
-2
lines changed

src/parser/expression.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,12 @@ pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) {
244244
if (this.match(tt.plusMin) && !this.isNextCharWhitespace()) {
245245
return left;
246246
}
247+
// if it's a `|` in a match/case on a newline, assume it's for the "case"
248+
// TODO: consider using indentation to be more precise about this
249+
// TODO: just remove all bitwise operators so this isn't necessary.
250+
if (this.match(tt.bitwiseOR) && this.state.inMatchCaseConsequent) {
251+
return left;
252+
}
247253
}
248254

249255
if (this.hasPlugin("lightscript") && this.isBitwiseOp()) {
@@ -288,7 +294,12 @@ pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) {
288294
// Parse unary operators, both prefix and postfix.
289295

290296
pp.parseMaybeUnary = function (refShorthandDefaultPos) {
291-
if (this.state.type.prefix) {
297+
const matchCaseBinaryPlusMin = this.hasPlugin("lightscript") &&
298+
this.state.inMatchCaseTest &&
299+
this.match(tt.plusMin) &&
300+
this.isNextCharWhitespace();
301+
302+
if (this.state.type.prefix && !matchCaseBinaryPlusMin) {
292303
if (this.hasPlugin("lightscript") && this.match(tt.plusMin)) {
293304
if (this.isNextCharWhitespace()) this.unexpected(null, "Unary +/- cannot be followed by a space in lightscript.");
294305
}
@@ -716,6 +727,11 @@ pp.parseExprAtom = function (refShorthandDefaultPos) {
716727
return this.parseIfExpression(node);
717728
}
718729

730+
case tt._match:
731+
if (this.hasPlugin("lightscript")) {
732+
return this.parseMatch();
733+
}
734+
719735
case tt.arrow:
720736
if (this.hasPlugin("lightscript")) {
721737
node = this.startNode();
@@ -741,6 +757,11 @@ pp.parseExprAtom = function (refShorthandDefaultPos) {
741757
}
742758

743759
default:
760+
if (this.hasPlugin("lightscript") && this.allowMatchCasePlaceholder()) {
761+
// use the blank space as an empty value (perhaps 0-length would be better)
762+
node = this.startNodeAt(this.state.lastTokEnd, this.state.lastTokEndLoc);
763+
return this.finishNodeAt(node, "PlaceholderExpression", this.state.start, this.state.startLoc);
764+
}
744765
this.unexpected();
745766
}
746767
};

src/plugins/jsx/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ export default function(instance) {
434434
return function(code) {
435435
if (this.state.inPropertyName) return inner.call(this, code);
436436

437+
// don't allow jsx inside match case tests
438+
if (this.hasPlugin("lightscript") && this.state.inMatchCaseTest) {
439+
return inner.call(this, code);
440+
}
441+
437442
const context = this.curContext();
438443

439444
if (this.hasPlugin("lightscript") && code === 60) {

src/plugins/lightscript.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,113 @@ pp.isBitwiseOp = function () {
550550
);
551551
};
552552

553+
pp.parseMatch = function () {
554+
const node = this.startNode();
555+
this.expect(tt._match);
556+
node.discriminant = this.parseParenExpression();
557+
558+
let isEnd;
559+
if (this.match(tt.colon)) {
560+
const indentLevel = this.state.indentLevel;
561+
this.next();
562+
isEnd = () => this.state.indentLevel <= indentLevel || this.match(tt.eof);
563+
} else {
564+
this.expect(tt.braceL);
565+
isEnd = () => this.eat(tt.braceR);
566+
}
567+
568+
node.cases = [];
569+
let hasUsedElse = false;
570+
while (!isEnd()) {
571+
if (hasUsedElse) {
572+
this.unexpected(null, "`else` must be last case.");
573+
}
574+
575+
const matchCase = this.parseMatchCase();
576+
if (matchCase.test && matchCase.test.type === "MatchElse") {
577+
hasUsedElse = true;
578+
}
579+
node.cases.push(matchCase);
580+
}
581+
582+
return this.finishNode(node, "MatchExpression");
583+
};
584+
585+
pp.parseMatchCase = function () {
586+
const node = this.startNode();
587+
588+
node.test = this.parseMatchCaseTest();
589+
this.expect(tt.comma);
590+
591+
// parse arrow function expression. (TODO: consider allowing NamedArrowExpression)
592+
const oldInMatchCaseConsequent = this.state.inMatchCaseConsequent;
593+
this.state.inMatchCaseConsequent = true;
594+
node.consequent = this.parseMaybeAssign();
595+
this.state.inMatchCaseConsequent = oldInMatchCaseConsequent;
596+
if (node.consequent.type !== "ArrowFunctionExpression") {
597+
this.unexpected(node.consequent.start, tt.arrow);
598+
}
599+
600+
return this.finishNode(node, "MatchCase");
601+
};
602+
603+
pp.parseMatchCaseTest = function () {
604+
// can't be nested so no need to read/restore old value
605+
this.state.inMatchCaseTest = true;
606+
607+
this.expect(tt.bitwiseOR);
608+
if (this.isLineBreak()) this.unexpected(this.state.lastTokEnd, "Illegal newline.");
609+
610+
let test;
611+
if (this.match(tt._else)) {
612+
const elseNode = this.startNode();
613+
this.next();
614+
test = this.finishNode(elseNode, "MatchElse");
615+
} else {
616+
test = this.parseExprOps();
617+
}
618+
619+
this.state.inMatchCaseTest = false;
620+
return test;
621+
};
622+
623+
pp.isBinaryTokenForMatchCase = function (tokenType) {
624+
return (
625+
tokenType.binop != null &&
626+
tokenType !== tt.logicalOR &&
627+
tokenType !== tt.logicalAND &&
628+
tokenType !== tt.bitwiseOR
629+
);
630+
};
631+
632+
pp.isSubscriptTokenForMatchCase = function (tokenType) {
633+
return (
634+
tokenType === tt.dot ||
635+
tokenType === tt.elvis ||
636+
tokenType === tt.tilde ||
637+
tokenType === tt.bracketL ||
638+
(tokenType === this.state.type && this.isNumberStartingWithDot())
639+
);
640+
};
641+
642+
pp.allowMatchCasePlaceholder = function () {
643+
if (!this.state.inMatchCaseTest) {
644+
return false;
645+
}
646+
const cur = this.state.type;
647+
const prev = this.state.tokens[this.state.tokens.length - 1].type;
648+
649+
// don't allow two binary tokens in a row to use placeholders, eg; `+ *`
650+
if (this.isBinaryTokenForMatchCase(cur)) {
651+
return !this.isBinaryTokenForMatchCase(prev);
652+
}
653+
// don't allow two subscripts in a row to use placeholders, eg; `..`
654+
if (this.isSubscriptTokenForMatchCase(cur)) {
655+
return !this.isSubscriptTokenForMatchCase(prev);
656+
}
657+
return false;
658+
};
659+
553660

554661
export default function (instance) {
555662

src/tokenizer/state.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export default class State {
3838
this.pos = this.lineStart = 0;
3939
this.curLine = options.startLine;
4040

41+
// for lightscript
4142
this.indentLevel = 0;
43+
this.inMatchCaseConsequent =
44+
this.inMatchCaseTest =
45+
false;
4246

4347
this.type = tt.eof;
4448
this.value = null;

src/tokenizer/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export const keywords = {
127127
"or": types.logicalOR,
128128
"and": types.logicalAND,
129129
"not": new KeywordTokenType("not", { beforeExpr, prefix, startsExpr }),
130+
"match": new KeywordTokenType("match", { beforeExpr, startsExpr }),
130131

131132
"break": new KeywordTokenType("break"),
132133
"case": new KeywordTokenType("case", { beforeExpr }),

src/util/identifier.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
match x:
2+
| a, -> 1
3+
| b, b -> b
4+
| c, (c) -> c
5+
| d, ({ d }) -> d
6+
| e, ({ e = 1 }) -> e
7+
| f, ([ f ]) -> f
8+
| g, ([ g = 1 ]) -> g
9+
| h, => 1
10+
| i, i => i
11+
| j, (j) => j

0 commit comments

Comments
 (0)