Browse Source

Implemented logical assignment operators. Closes #647.

Dmitry Panov 7 months ago
parent
commit
203961f822
8 changed files with 111 additions and 31 deletions
  1. 1 1
      .github/workflows/main.yml
  2. 41 3
      compiler_expr.go
  3. 8 8
      compiler_stmt.go
  4. 6 0
      parser/expression.go
  5. 8 3
      parser/lexer.go
  6. 0 1
      tc39_test.go
  7. 7 0
      token/token_const.go
  8. 40 15
      vm.go

+ 1 - 1
.github/workflows/main.yml

@@ -24,7 +24,7 @@ jobs:
       - name: Run staticcheck
         uses: dominikh/[email protected]
         with:
-          version: "2024.1.1"
+          version: "2025.1"
           install-go: false
           cache-key: ${{ matrix.go-version }}
         if: ${{ matrix.go-version == '1.x' }}

+ 41 - 3
compiler_expr.go

@@ -1255,6 +1255,44 @@ func (e *compiledAssignExpr) emitGetter(putOnStack bool) {
 			e.right.emitGetter(true)
 			e.c.emit(shr)
 		}, false, putOnStack)
+	case token.LOGICAL_AND, token.LOGICAL_OR, token.COALESCE:
+		e.left.emitRef()
+		e.c.emit(getValue)
+		mark := len(e.c.p.code)
+		e.c.emit(nil)
+		if id, ok := e.left.(*compiledIdentifierExpr); ok {
+			e.c.emitNamedOrConst(e.right, id.name)
+		} else {
+			e.right.emitGetter(true)
+		}
+		if putOnStack {
+			e.c.emit(putValue)
+		} else {
+			e.c.emit(putValueP)
+		}
+		e.c.emit(jump(2))
+		offset := len(e.c.p.code) - mark
+		switch e.operator {
+		case token.LOGICAL_AND:
+			if putOnStack {
+				e.c.p.code[mark] = jne(offset)
+			} else {
+				e.c.p.code[mark] = jneP(offset)
+			}
+		case token.LOGICAL_OR:
+			if putOnStack {
+				e.c.p.code[mark] = jeq(offset)
+			} else {
+				e.c.p.code[mark] = jeqP(offset)
+			}
+		case token.COALESCE:
+			if putOnStack {
+				e.c.p.code[mark] = jcoalesc(offset)
+			} else {
+				e.c.p.code[mark] = jcoalescP(offset)
+			}
+		}
+		e.c.emit(popRef)
 	default:
 		e.c.assert(false, e.offset, "Unknown assign operator: %s", e.operator.String())
 		panic("unreachable")
@@ -2598,7 +2636,7 @@ func (e *compiledConditionalExpr) emitGetter(putOnStack bool) {
 	e.consequent.emitGetter(putOnStack)
 	j1 := len(e.c.p.code)
 	e.c.emit(nil)
-	e.c.p.code[j] = jne(len(e.c.p.code) - j)
+	e.c.p.code[j] = jneP(len(e.c.p.code) - j)
 	e.alternate.emitGetter(putOnStack)
 	e.c.p.code[j1] = jump(len(e.c.p.code) - j1)
 }
@@ -2648,7 +2686,7 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) {
 	e.addSrcMap()
 	e.c.emit(nil)
 	e.c.emitExpr(e.right, true)
-	e.c.p.code[j] = jeq1(len(e.c.p.code) - j)
+	e.c.p.code[j] = jeq(len(e.c.p.code) - j)
 	if !putOnStack {
 		e.c.emit(pop)
 	}
@@ -2730,7 +2768,7 @@ func (e *compiledLogicalAnd) emitGetter(putOnStack bool) {
 	e.addSrcMap()
 	e.c.emit(nil)
 	e.c.emitExpr(e.right, true)
-	e.c.p.code[j] = jneq1(len(e.c.p.code) - j)
+	e.c.p.code[j] = jne(len(e.c.p.code) - j)
 	if !putOnStack {
 		e.c.emit(pop)
 	}

+ 8 - 8
compiler_stmt.go

@@ -226,7 +226,7 @@ func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needR
 	c.compileStatement(v.Body, needResult)
 	c.block.cont = len(c.p.code)
 	c.emitExpr(c.compileExpression(v.Test), true)
-	c.emit(jeq(start - len(c.p.code)))
+	c.emit(jeqP(start - len(c.p.code)))
 	c.leaveBlock()
 }
 
@@ -339,7 +339,7 @@ func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bo
 	c.emit(jump(start - len(c.p.code)))
 	if v.Test != nil {
 		if !testConst {
-			c.p.code[j] = jne(len(c.p.code) - j)
+			c.p.code[j] = jneP(len(c.p.code) - j)
 		}
 	}
 end:
@@ -535,7 +535,7 @@ func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResul
 	c.compileStatement(v.Body, needResult)
 	c.emit(jump(start - len(c.p.code)))
 	if !testTrue {
-		c.p.code[j] = jne(len(c.p.code) - j)
+		c.p.code[j] = jneP(len(c.p.code) - j)
 	}
 end:
 	c.leaveBlock()
@@ -713,16 +713,16 @@ func (c *compiler) compileIfStatement(v *ast.IfStatement, needResult bool) {
 	if v.Alternate != nil {
 		jmp1 := len(c.p.code)
 		c.emit(nil)
-		c.p.code[jmp] = jne(len(c.p.code) - jmp)
+		c.p.code[jmp] = jneP(len(c.p.code) - jmp)
 		c.compileIfBody(v.Alternate, needResult)
 		c.p.code[jmp1] = jump(len(c.p.code) - jmp1)
 	} else {
 		if needResult {
 			c.emit(jump(2))
-			c.p.code[jmp] = jne(len(c.p.code) - jmp)
+			c.p.code[jmp] = jneP(len(c.p.code) - jmp)
 			c.emit(clearResult)
 		} else {
-			c.p.code[jmp] = jne(len(c.p.code) - jmp)
+			c.p.code[jmp] = jneP(len(c.p.code) - jmp)
 		}
 	}
 }
@@ -1081,9 +1081,9 @@ func (c *compiler) compileSwitchStatement(v *ast.SwitchStatement, needResult boo
 			c.compileExpression(s.Test).emitGetter(true)
 			c.emit(op_strict_eq)
 			if db != nil {
-				c.emit(jne(2))
+				c.emit(jneP(2))
 			} else {
-				c.emit(jne(3), pop)
+				c.emit(jneP(3), pop)
 			}
 			jumps[i] = len(c.p.code)
 			c.emit(nil)

+ 6 - 0
parser/expression.go

@@ -1287,6 +1287,12 @@ func (self *_parser) parseAssignmentExpression() ast.Expression {
 		operator = token.SHIFT_RIGHT
 	case token.UNSIGNED_SHIFT_RIGHT_ASSIGN:
 		operator = token.UNSIGNED_SHIFT_RIGHT
+	case token.LOGICAL_AND_ASSIGN:
+		operator = token.LOGICAL_AND
+	case token.LOGICAL_OR_ASSIGN:
+		operator = token.LOGICAL_OR
+	case token.COALESCE_ASSIGN:
+		operator = token.COALESCE
 	case token.ARROW:
 		var paramList *ast.ParameterList
 		if id, ok := left.(*ast.Identifier); ok {

+ 8 - 3
parser/lexer.go

@@ -409,9 +409,9 @@ func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unis
 					tkn = token.STRICT_NOT_EQUAL
 				}
 			case '&':
-				tkn = self.switch3(token.AND, token.AND_ASSIGN, '&', token.LOGICAL_AND)
+				tkn = self.switch4(token.AND, token.AND_ASSIGN, '&', token.LOGICAL_AND, token.LOGICAL_AND_ASSIGN)
 			case '|':
-				tkn = self.switch3(token.OR, token.OR_ASSIGN, '|', token.LOGICAL_OR)
+				tkn = self.switch4(token.OR, token.OR_ASSIGN, '|', token.LOGICAL_OR, token.LOGICAL_OR_ASSIGN)
 			case '~':
 				tkn = token.BITWISE_NOT
 			case '?':
@@ -420,7 +420,12 @@ func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unis
 					tkn = token.QUESTION_DOT
 				} else if self.chr == '?' {
 					self.read()
-					tkn = token.COALESCE
+					if self.chr == '=' {
+						self.read()
+						tkn = token.COALESCE_ASSIGN
+					} else {
+						tkn = token.COALESCE
+					}
 				} else {
 					tkn = token.QUESTION_MARK
 				}

+ 0 - 1
tc39_test.go

@@ -209,7 +209,6 @@ var (
 		"Temporal",
 		"import-assertions",
 		"dynamic-import",
-		"logical-assignment-operators",
 		"import.meta",
 		"Atomics",
 		"Atomics.waitAsync",

+ 7 - 0
token/token_const.go

@@ -44,6 +44,10 @@ const (
 	INCREMENT   // ++
 	DECREMENT   // --
 
+	LOGICAL_AND_ASSIGN // &&=
+	LOGICAL_OR_ASSIGN  // ||=
+	COALESCE_ASSIGN    // ??=
+
 	EQUAL        // ==
 	STRICT_EQUAL // ===
 	LESS         // <
@@ -173,6 +177,9 @@ var token2string = [...]string{
 	COALESCE:                    "??",
 	INCREMENT:                   "++",
 	DECREMENT:                   "--",
+	LOGICAL_AND_ASSIGN:          "&&=",
+	LOGICAL_OR_ASSIGN:           "||=",
+	COALESCE_ASSIGN:             "??=",
 	EQUAL:                       "==",
 	STRICT_EQUAL:                "===",
 	LESS:                        "<",

+ 40 - 15
vm.go

@@ -259,7 +259,14 @@ type objStrRef struct {
 }
 
 func (r *objStrRef) get() Value {
-	return r.base.self.getStr(r.name, r.this)
+	if v := r.base.self.getStr(r.name, r.this); v != nil {
+		return v
+	}
+	if r.binding {
+		rt := r.base.runtime
+		panic(rt.newReferenceError(r.name))
+	}
+	return _undefined
 }
 
 func (r *objStrRef) set(v Value) {
@@ -3382,12 +3389,7 @@ var getValue _getValue
 
 func (_getValue) exec(vm *vm) {
 	ref := vm.refStack[len(vm.refStack)-1]
-	if v := ref.get(); v != nil {
-		vm.push(v)
-	} else {
-		vm.throw(vm.r.newReferenceError(ref.refname()))
-		return
-	}
+	vm.push(nilSafe(ref.get()))
 	vm.pc++
 }
 
@@ -3404,6 +3406,17 @@ func (_putValue) exec(vm *vm) {
 	vm.pc++
 }
 
+type _popRef struct{}
+
+var popRef _popRef
+
+func (_popRef) exec(vm *vm) {
+	l := len(vm.refStack) - 1
+	vm.refStack[l] = nil
+	vm.refStack = vm.refStack[:l]
+	vm.pc++
+}
+
 type _putValueP struct{}
 
 var putValueP _putValueP
@@ -4282,9 +4295,9 @@ func (b *bindGlobal) exec(vm *vm) {
 	vm.pc++
 }
 
-type jne int32
+type jneP int32
 
-func (j jne) exec(vm *vm) {
+func (j jneP) exec(vm *vm) {
 	vm.sp--
 	if !vm.stack[vm.sp].ToBoolean() {
 		vm.pc += int(j)
@@ -4293,9 +4306,9 @@ func (j jne) exec(vm *vm) {
 	}
 }
 
-type jeq int32
+type jeqP int32
 
-func (j jeq) exec(vm *vm) {
+func (j jeqP) exec(vm *vm) {
 	vm.sp--
 	if vm.stack[vm.sp].ToBoolean() {
 		vm.pc += int(j)
@@ -4304,9 +4317,9 @@ func (j jeq) exec(vm *vm) {
 	}
 }
 
-type jeq1 int32
+type jeq int32
 
-func (j jeq1) exec(vm *vm) {
+func (j jeq) exec(vm *vm) {
 	if vm.stack[vm.sp-1].ToBoolean() {
 		vm.pc += int(j)
 	} else {
@@ -4315,9 +4328,9 @@ func (j jeq1) exec(vm *vm) {
 	}
 }
 
-type jneq1 int32
+type jne int32
 
-func (j jneq1) exec(vm *vm) {
+func (j jne) exec(vm *vm) {
 	if !vm.stack[vm.sp-1].ToBoolean() {
 		vm.pc += int(j)
 	} else {
@@ -4387,6 +4400,18 @@ func (j jcoalesc) exec(vm *vm) {
 	}
 }
 
+type jcoalescP int32
+
+func (j jcoalescP) exec(vm *vm) {
+	vm.sp--
+	switch vm.stack[vm.sp] {
+	case _undefined, _null:
+		vm.pc++
+	default:
+		vm.pc += int(j)
+	}
+}
+
 type _not struct{}
 
 var not _not