From fa83eb819e14334355c2c493fa6fa6a560aa036b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sun, 24 Apr 2022 13:00:09 +0100 Subject: [PATCH] Implemented nullish coalescing operator (??). Closes #382. (cherry picked from commit 160b8c59fd7590edcb8ccc725f1e2cfb3464a203) --- compiler_expr.go | 59 +++++++++++++++++++++++++++++++++++++++++-- parser/expression.go | 59 ++++++++++++++++++++++++++++++++++++++----- parser/lexer.go | 3 +++ parser/parser_test.go | 9 +++++++ tc39_test.go | 1 - token/token_const.go | 2 ++ vm.go | 14 ++++++++++ 7 files changed, 137 insertions(+), 10 deletions(-) diff --git a/compiler_expr.go b/compiler_expr.go index 21a297a2..f060c395 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -163,6 +163,11 @@ type compiledLogicalOr struct { left, right compiledExpr } +type compiledCoalesce struct { + baseCompiledExpr + left, right compiledExpr +} + type compiledLogicalAnd struct { baseCompiledExpr left, right compiledExpr @@ -1630,7 +1635,6 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) { j := len(e.c.p.code) e.addSrcMap() e.c.emit(nil) - e.c.emit(pop) e.c.emitExpr(e.right, true) e.c.p.code[j] = jeq1(len(e.c.p.code) - j) if !putOnStack { @@ -1638,6 +1642,47 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) { } } +func (e *compiledCoalesce) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v != _null && v != _undefined { + return true + } + return e.right.constant() + } else { + return true + } + } + + return false +} + +func (e *compiledCoalesce) emitGetter(putOnStack bool) { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v == _undefined || v == _null { + e.c.emitExpr(e.right, putOnStack) + } else { + if putOnStack { + e.c.emit(loadVal(e.c.p.defineLiteralValue(v))) + } + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.c.emitExpr(e.left, true) + j := len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jcoalesc(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + func (e *compiledLogicalAnd) constant() bool { if e.left.constant() { if v, ex := e.c.evalConst(e.left); ex == nil { @@ -1672,7 +1717,6 @@ func (e *compiledLogicalAnd) emitGetter(putOnStack bool) { j = len(e.c.p.code) e.addSrcMap() e.c.emit(nil) - e.c.emit(pop) e.c.emitExpr(e.right, true) e.c.p.code[j] = jneq1(len(e.c.p.code) - j) if !putOnStack { @@ -1748,6 +1792,8 @@ func (c *compiler) compileBinaryExpression(v *ast.BinaryExpression) compiledExpr switch v.Operator { case token.LOGICAL_OR: return c.compileLogicalOr(v.Left, v.Right, v.Idx0()) + case token.COALESCE: + return c.compileCoalesce(v.Left, v.Right, v.Idx0()) case token.LOGICAL_AND: return c.compileLogicalAnd(v.Left, v.Right, v.Idx0()) } @@ -1770,6 +1816,15 @@ func (c *compiler) compileLogicalOr(left, right ast.Expression, idx file.Idx) co return r } +func (c *compiler) compileCoalesce(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledCoalesce{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + func (c *compiler) compileLogicalAnd(left, right ast.Expression, idx file.Idx) compiledExpr { r := &compiledLogicalAnd{ left: c.compileExpression(left), diff --git a/parser/expression.go b/parser/expression.go index f863a308..ca13e702 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -954,19 +954,64 @@ func (self *_parser) parseLogicalAndExpression() ast.Expression { return left } +func isLogicalAndExpr(expr ast.Expression) bool { + if bexp, ok := expr.(*ast.BinaryExpression); ok && bexp.Operator == token.LOGICAL_AND { + return true + } + return false +} + func (self *_parser) parseLogicalOrExpression() ast.Expression { + var idx file.Idx + parenthesis := self.token == token.LEFT_PARENTHESIS left := self.parseLogicalAndExpression() - for self.token == token.LOGICAL_OR { - tkn := self.token - self.next() - left = &ast.BinaryExpression{ - Operator: tkn, - Left: left, - Right: self.parseLogicalAndExpression(), + if self.token == token.LOGICAL_OR || !parenthesis && isLogicalAndExpr(left) { + for { + switch self.token { + case token.LOGICAL_OR: + self.next() + left = &ast.BinaryExpression{ + Operator: token.LOGICAL_OR, + Left: left, + Right: self.parseLogicalAndExpression(), + } + case token.COALESCE: + idx = self.idx + goto mixed + default: + return left + } + } + } else { + for { + switch self.token { + case token.COALESCE: + idx = self.idx + self.next() + + parenthesis := self.token == token.LEFT_PARENTHESIS + right := self.parseLogicalAndExpression() + if !parenthesis && isLogicalAndExpr(right) { + goto mixed + } + + left = &ast.BinaryExpression{ + Operator: token.COALESCE, + Left: left, + Right: right, + } + case token.LOGICAL_OR: + idx = self.idx + goto mixed + default: + return left + } } } +mixed: + self.error(idx, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") return left } diff --git a/parser/lexer.go b/parser/lexer.go index 1c8807be..fef1aec7 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -413,6 +413,9 @@ func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unis if self.chr == '.' && !isDecimalDigit(self._peek()) { self.read() tkn = token.QUESTION_DOT + } else if self.chr == '?' { + self.read() + tkn = token.COALESCE } else { tkn = token.QUESTION_MARK } diff --git a/parser/parser_test.go b/parser/parser_test.go index 19abe866..28785abc 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -503,6 +503,9 @@ func TestParserErr(t *testing.T) { test(`var{..(`, "(anonymous): Line 1:7 Unexpected token ILLEGAL") test(`var{get..(`, "(anonymous): Line 1:10 Unexpected token ILLEGAL") test(`var{set..(`, "(anonymous): Line 1:10 Unexpected token ILLEGAL") + test(`(0 ?? 0 || true)`, "(anonymous): Line 1:9 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + test(`(a || b ?? c)`, "(anonymous): Line 1:9 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + test(`2 ?? 2 && 3 + 3`, "(anonymous): Line 1:3 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") }) } @@ -902,6 +905,12 @@ func TestParser(t *testing.T) { test(`ref = (a, b = 39,) => { };`, nil) test(`(a,) => {}`, nil) + + test(`2 ?? (2 && 3) + 3`, nil) + test(`(2 ?? 2) && 3 + 3`, nil) + program = test(`a ?? b ?? c`, nil) + is(len(program.Body), 1) + is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right.(*ast.Identifier).Name, "c") }) } diff --git a/tc39_test.go b/tc39_test.go index 251d234c..7e054494 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -233,7 +233,6 @@ var ( "import-assertions", "dynamic-import", "logical-assignment-operators", - "coalesce-expression", "import.meta", "Atomics", "Atomics.waitAsync", diff --git a/token/token_const.go b/token/token_const.go index da24d034..e25af589 100644 --- a/token/token_const.go +++ b/token/token_const.go @@ -40,6 +40,7 @@ const ( LOGICAL_AND // && LOGICAL_OR // || + COALESCE // ?? INCREMENT // ++ DECREMENT // -- @@ -155,6 +156,7 @@ var token2string = [...]string{ UNSIGNED_SHIFT_RIGHT_ASSIGN: ">>>=", LOGICAL_AND: "&&", LOGICAL_OR: "||", + COALESCE: "??", INCREMENT: "++", DECREMENT: "--", EQUAL: "==", diff --git a/vm.go b/vm.go index 4df17580..87b22216 100644 --- a/vm.go +++ b/vm.go @@ -3324,6 +3324,7 @@ func (j jeq1) exec(vm *vm) { if vm.stack[vm.sp-1].ToBoolean() { vm.pc += int(j) } else { + vm.sp-- vm.pc++ } } @@ -3334,6 +3335,7 @@ func (j jneq1) exec(vm *vm) { if !vm.stack[vm.sp-1].ToBoolean() { vm.pc += int(j) } else { + vm.sp-- vm.pc++ } } @@ -3374,6 +3376,18 @@ func (j jopt) exec(vm *vm) { } } +type jcoalesc int32 + +func (j jcoalesc) exec(vm *vm) { + switch vm.stack[vm.sp-1] { + case _undefined, _null: + vm.sp-- + vm.pc++ + default: + vm.pc += int(j) + } +} + type _not struct{} var not _not