Skip to content

Commit

Permalink
Implemented nullish coalescing operator (??). Closes dop251#382.
Browse files Browse the repository at this point in the history
(cherry picked from commit 160b8c5)
  • Loading branch information
dop251 authored and Gabri3l committed Sep 1, 2022
1 parent 49b5f5f commit fa83eb8
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 10 deletions.
59 changes: 57 additions & 2 deletions compiler_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ type compiledLogicalOr struct {
left, right compiledExpr
}

type compiledCoalesce struct {
baseCompiledExpr
left, right compiledExpr
}

type compiledLogicalAnd struct {
baseCompiledExpr
left, right compiledExpr
Expand Down Expand Up @@ -1630,14 +1635,54 @@ 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 {
e.c.emit(pop)
}
}

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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
}
Expand All @@ -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),
Expand Down
59 changes: 52 additions & 7 deletions parser/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions parser/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 9 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}

Expand Down Expand Up @@ -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")
})
}

Expand Down
1 change: 0 additions & 1 deletion tc39_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ var (
"import-assertions",
"dynamic-import",
"logical-assignment-operators",
"coalesce-expression",
"import.meta",
"Atomics",
"Atomics.waitAsync",
Expand Down
2 changes: 2 additions & 0 deletions token/token_const.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (

LOGICAL_AND // &&
LOGICAL_OR // ||
COALESCE // ??
INCREMENT // ++
DECREMENT // --

Expand Down Expand Up @@ -155,6 +156,7 @@ var token2string = [...]string{
UNSIGNED_SHIFT_RIGHT_ASSIGN: ">>>=",
LOGICAL_AND: "&&",
LOGICAL_OR: "||",
COALESCE: "??",
INCREMENT: "++",
DECREMENT: "--",
EQUAL: "==",
Expand Down
14 changes: 14 additions & 0 deletions vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
}
Expand All @@ -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++
}
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit fa83eb8

Please sign in to comment.