Browse Source

Implemented nullish coalescing operator (??). Closes #382.

Dmitry Panov 3 years ago
parent
commit
160b8c59fd
7 changed files with 137 additions and 10 deletions
  1. 57 2
      compiler_expr.go
  2. 52 7
      parser/expression.go
  3. 3 0
      parser/lexer.go
  4. 9 0
      parser/parser_test.go
  5. 0 1
      tc39_test.go
  6. 2 0
      token/token_const.go
  7. 14 0
      vm.go

+ 57 - 2
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),

+ 52 - 7
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
 }
 

+ 3 - 0
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
 				}

+ 9 - 0
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")
 	})
 }
 

+ 0 - 1
tc39_test.go

@@ -233,7 +233,6 @@ var (
 		"import-assertions",
 		"dynamic-import",
 		"logical-assignment-operators",
-		"coalesce-expression",
 		"import.meta",
 		"Atomics",
 		"Atomics.waitAsync",

+ 2 - 0
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:                       "==",

+ 14 - 0
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