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

Pattern Matching #17

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
14 changes: 13 additions & 1 deletion src/parser/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,14 @@ pp.parseExprOps = function (noIn, refShorthandDefaultPos) {
pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) {
// correct ASI failures.
if (this.hasPlugin("lightscript") && this.isLineBreak()) {

// if it's a newline followed by a unary +/-, bail so it can be parsed separately.
if (this.match(tt.plusMin) && !this.isNextCharWhitespace()) {
return left;
}
// for match/case
if (this.match(tt.bitwiseOR)) {
return left;
}
}

if (this.hasPlugin("lightscript") && this.isBitwiseOp()) {
Expand Down Expand Up @@ -716,6 +719,12 @@ pp.parseExprAtom = function (refShorthandDefaultPos) {
return this.parseIfExpression(node);
}

case tt._match:
if (this.hasPlugin("lightscript")) {
node = this.startNode();
return this.parseMatchExpression(node);
}

case tt.arrow:
if (this.hasPlugin("lightscript")) {
node = this.startNode();
Expand Down Expand Up @@ -1151,6 +1160,9 @@ pp.parsePropertyName = function (prop) {
// Initialize empty function node.

pp.initFunction = function (node, isAsync) {
if (this.hasPlugin("lightscript") && this.state.inMatchCaseTest) {
this.unexpected(node.start, "Cannot match on functions.");
}
node.id = null;
node.generator = false;
node.expression = false;
Expand Down
3 changes: 3 additions & 0 deletions src/parser/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ pp.parseStatement = function (declaration, topLevel) {
}
return starttype === tt._import ? this.parseImport(node) : this.parseExport(node);

case tt._match:
if (this.hasPlugin("lightscript")) return this.parseMatchStatement(node);

case tt.name:
if (this.state.value === "async") {
// peek ahead and see if next token is a function
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/jsx/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ export default function(instance) {
return function(code) {
if (this.state.inPropertyName) return inner.call(this, code);

// don't allow jsx inside match case tests
if (this.hasPlugin("lightscript") && this.state.inMatchCaseTest) {
return inner.call(this, code);
}

const context = this.curContext();

if (this.hasPlugin("lightscript") && code === 60) {
Expand Down
112 changes: 110 additions & 2 deletions src/plugins/lightscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ pp.expectParenFreeBlockStart = function (node) {
// if true: blah
// if true { blah }
// if (true) blah
// match (foo) as bar:
if (node && node.extra && node.extra.hasParens) {
this.expect(tt.parenR);
} else if (!(this.match(tt.colon) || this.match(tt.braceL))) {
} else if (!(this.match(tt.colon) || this.match(tt.braceL) || this.isContextual("as"))) {
this.unexpected(null, "Paren-free test expressions must be followed by braces or a colon.");
}
};
Expand Down Expand Up @@ -550,6 +551,112 @@ pp.isBitwiseOp = function () {
);
};

pp.parseMatchExpression = function (node) {
return this.parseMatch(node, true);
};

pp.parseMatchStatement = function (node) {
return this.parseMatch(node, false);
};

pp.parseMatch = function (node, isExpression) {
if (this.state.inMatchCaseTest) this.unexpected();
this.expect(tt._match);

node.discriminant = this.parseParenExpression();
if (this.eatContextual("as")) {
node.alias = this.parseIdentifier();
}

const isColon = this.match(tt.colon);
let isEnd;
if (isColon) {
const indentLevel = this.state.indentLevel;
this.next();
isEnd = () => this.state.indentLevel <= indentLevel || this.match(tt.eof);
} else {
this.expect(tt.braceL);
isEnd = () => this.eat(tt.braceR);
}

node.cases = [];
const caseIndentLevel = this.state.indentLevel;
let hasUsedElse = false;
while (!isEnd()) {
if (hasUsedElse) {
this.unexpected(null, "`else` must be last case.");
}
if (isColon && this.state.indentLevel !== caseIndentLevel) {
this.unexpected(null, "Mismatched indent.");
}

const matchCase = this.parseMatchCase(isExpression);
if (matchCase.test && matchCase.test.type === "MatchElse") {
hasUsedElse = true;
}
node.cases.push(matchCase);
}

if (!node.cases.length) {
this.unexpected(null, tt.bitwiseOR);
}

return this.finishNode(node, isExpression ? "MatchExpression" : "MatchStatement");
};

pp.parseMatchCase = function (isExpression) {
const node = this.startNode();

this.parseMatchCaseTest(node);

if (isExpression) {
// disallow return/continue/break, etc. c/p doExpression
const oldInFunction = this.state.inFunction;
const oldLabels = this.state.labels;
this.state.labels = [];
this.state.inFunction = false;

node.consequent = this.parseBlock(false);

this.state.inFunction = oldInFunction;
this.state.labels = oldLabels;
} else {
node.consequent = this.parseBlock(false);
}

return this.finishNode(node, "MatchCase");
};

pp.parseMatchCaseTest = function (node) {
// can't be nested so no need to read/restore old value
this.state.inMatchCaseTest = true;

this.expect(tt.bitwiseOR);
if (this.isLineBreak()) this.unexpected(this.state.lastTokEnd, "Illegal newline.");

if (this.match(tt._else)) {
const elseNode = this.startNode();
this.next();
node.test = this.finishNode(elseNode, "MatchElse");
} else if (this.eat(tt._with)) {
this.parseMatchCaseBinding(node);
} else {
node.test = this.parseExprOps();
}

if (this.eat(tt._with)) {
this.parseMatchCaseBinding(node);
}

this.state.inMatchCaseTest = false;
};

pp.parseMatchCaseBinding = function (node) {
if (node.binding) this.unexpected(this.state.lastTokStart, "Cannot destructure twice.");
if (!(this.match(tt.braceL) || this.match(tt.bracketL))) this.unexpected();
node.binding = this.parseBindingAtom();
};


export default function (instance) {

Expand All @@ -568,7 +675,8 @@ export default function (instance) {
// first, try paren-free style
try {
const val = this.parseExpression();
if (this.match(tt.braceL) || this.match(tt.colon)) {
// "as" for `match (foo) as bar:`, bit dirty to allow for all but not a problem
if (this.match(tt.braceL) || this.match(tt.colon) || this.isContextual("as")) {
if (val.extra && val.extra.parenthesized) {
delete val.extra.parenthesized;
delete val.extra.parenStart;
Expand Down
2 changes: 2 additions & 0 deletions src/tokenizer/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export default class State {
this.pos = this.lineStart = 0;
this.curLine = options.startLine;

// for lightscript
this.indentLevel = 0;
this.inMatchCaseTest = false;

this.type = tt.eof;
this.value = null;
Expand Down
1 change: 1 addition & 0 deletions src/tokenizer/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const keywords = {
"or": types.logicalOR,
"and": types.logicalAND,
"not": new KeywordTokenType("not", { beforeExpr, prefix, startsExpr }),
"match": new KeywordTokenType("match", { beforeExpr, startsExpr }),

"break": new KeywordTokenType("break"),
"case": new KeywordTokenType("case", { beforeExpr }),
Expand Down
2 changes: 1 addition & 1 deletion src/util/identifier.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions test/fixtures/lightscript/commaless/obj-pattern/actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
a
b: {
c
d
d = 1
e
}
e
} = f
f
...g
} = h
Loading