Browse Source

Block-scoped declarations (#264)

* Implemented block scoped declarations (let and const). Implemented optional catch binding. Lots of other fixes. Closes #179. Closes #259.

* Fixed 'let' parsing (see #179)

* Fixed lexical bindings in global context (see #179)

* Fixed global function bindings and for-into lexical bindings with initialisers (see #179)
Dmitry Panov 4 years ago
parent
commit
f3cfc97811
21 changed files with 3163 additions and 1192 deletions
  1. 90 28
      ast/node.go
  2. 3 4
      builtin_array.go
  3. 2 2
      builtin_regexp.go
  4. 1 1
      builtin_string.go
  5. 604 192
      compiler.go
  6. 189 174
      compiler_expr.go
  7. 432 322
      compiler_stmt.go
  8. 851 58
      compiler_test.go
  9. 17 8
      parser/expression.go
  10. 9 0
      parser/lexer.go
  11. 16 5
      parser/marshal_test.go
  12. 4 4
      parser/parser_test.go
  13. 3 2
      parser/scope.go
  14. 136 70
      parser/statement.go
  15. 106 65
      runtime.go
  16. 71 1
      runtime_test.go
  17. 69 46
      tc39_test.go
  18. 7 5
      token/token_const.go
  19. 38 14
      value.go
  20. 511 188
      vm.go
  21. 4 3
      vm_test.go

+ 90 - 28
ast/node.go

@@ -91,10 +91,10 @@ type (
 		Function      file.Idx
 		Name          *Identifier
 		ParameterList *ParameterList
-		Body          Statement
+		Body          *BlockStatement
 		Source        string
 
-		DeclarationList []Declaration
+		DeclarationList []*VariableDeclaration
 	}
 
 	Identifier struct {
@@ -241,7 +241,7 @@ type (
 	CatchStatement struct {
 		Catch     file.Idx
 		Parameter *Identifier
-		Body      Statement
+		Body      *BlockStatement
 	}
 
 	DebuggerStatement struct {
@@ -264,21 +264,21 @@ type (
 
 	ForInStatement struct {
 		For    file.Idx
-		Into   Expression
+		Into   ForInto
 		Source Expression
 		Body   Statement
 	}
 
 	ForOfStatement struct {
 		For    file.Idx
-		Into   Expression
+		Into   ForInto
 		Source Expression
 		Body   Statement
 	}
 
 	ForStatement struct {
 		For         file.Idx
-		Initializer Expression
+		Initializer ForLoopInitializer
 		Update      Expression
 		Test        Expression
 		Body        Statement
@@ -316,14 +316,20 @@ type (
 
 	TryStatement struct {
 		Try     file.Idx
-		Body    Statement
+		Body    *BlockStatement
 		Catch   *CatchStatement
-		Finally Statement
+		Finally *BlockStatement
 	}
 
 	VariableStatement struct {
 		Var  file.Idx
-		List []Expression
+		List []*VariableExpression
+	}
+
+	LexicalDeclaration struct {
+		Idx   file.Idx
+		Token token.Token
+		List  []*VariableExpression
 	}
 
 	WhileStatement struct {
@@ -337,6 +343,10 @@ type (
 		Object Expression
 		Body   Statement
 	}
+
+	FunctionDeclaration struct {
+		Function *FunctionLiteral
+	}
 )
 
 // _statementNode
@@ -362,31 +372,75 @@ func (*TryStatement) _statementNode()        {}
 func (*VariableStatement) _statementNode()   {}
 func (*WhileStatement) _statementNode()      {}
 func (*WithStatement) _statementNode()       {}
+func (*LexicalDeclaration) _statementNode()  {}
+func (*FunctionDeclaration) _statementNode() {}
 
 // =========== //
 // Declaration //
 // =========== //
 
 type (
-	// All declaration nodes implement the Declaration interface.
-	Declaration interface {
-		_declarationNode()
+	VariableDeclaration struct {
+		Var  file.Idx
+		List []*VariableExpression
 	}
+)
 
-	FunctionDeclaration struct {
-		Function *FunctionLiteral
+type (
+	ForLoopInitializer interface {
+		_forLoopInitializer()
 	}
 
-	VariableDeclaration struct {
+	ForLoopInitializerExpression struct {
+		Expression Expression
+	}
+
+	ForLoopInitializerVarDeclList struct {
 		Var  file.Idx
 		List []*VariableExpression
 	}
+
+	ForLoopInitializerLexicalDecl struct {
+		LexicalDeclaration LexicalDeclaration
+	}
+
+	ForInto interface {
+		_forInto()
+	}
+
+	ForIntoVar struct {
+		Binding *VariableExpression
+	}
+
+	ForBinding interface {
+		_forBinding()
+	}
+
+	BindingIdentifier struct {
+		Idx  file.Idx
+		Name unistring.String
+	}
+
+	ForDeclaration struct {
+		Idx     file.Idx
+		IsConst bool
+		Binding ForBinding
+	}
+
+	ForIntoExpression struct {
+		Expression Expression
+	}
 )
 
-// _declarationNode
+func (*ForLoopInitializerExpression) _forLoopInitializer()  {}
+func (*ForLoopInitializerVarDeclList) _forLoopInitializer() {}
+func (*ForLoopInitializerLexicalDecl) _forLoopInitializer() {}
 
-func (*FunctionDeclaration) _declarationNode() {}
-func (*VariableDeclaration) _declarationNode() {}
+func (*ForIntoVar) _forInto()        {}
+func (*ForDeclaration) _forInto()    {}
+func (*ForIntoExpression) _forInto() {}
+
+func (*BindingIdentifier) _forBinding() {}
 
 // ==== //
 // Node //
@@ -395,7 +449,7 @@ func (*VariableDeclaration) _declarationNode() {}
 type Program struct {
 	Body []Statement
 
-	DeclarationList []Declaration
+	DeclarationList []*VariableDeclaration
 
 	File *file.File
 }
@@ -449,6 +503,10 @@ func (self *TryStatement) Idx0() file.Idx        { return self.Try }
 func (self *VariableStatement) Idx0() file.Idx   { return self.Var }
 func (self *WhileStatement) Idx0() file.Idx      { return self.While }
 func (self *WithStatement) Idx0() file.Idx       { return self.With }
+func (self *LexicalDeclaration) Idx0() file.Idx  { return self.Idx }
+func (self *FunctionDeclaration) Idx0() file.Idx { return self.Function.Idx0() }
+
+func (self *ForLoopInitializerVarDeclList) Idx0() file.Idx { return self.List[0].Idx0() }
 
 // ==== //
 // Idx1 //
@@ -507,12 +565,16 @@ func (self *IfStatement) Idx1() file.Idx {
 	}
 	return self.Consequent.Idx1()
 }
-func (self *LabelledStatement) Idx1() file.Idx { return self.Colon + 1 }
-func (self *Program) Idx1() file.Idx           { return self.Body[len(self.Body)-1].Idx1() }
-func (self *ReturnStatement) Idx1() file.Idx   { return self.Return }
-func (self *SwitchStatement) Idx1() file.Idx   { return self.Body[len(self.Body)-1].Idx1() }
-func (self *ThrowStatement) Idx1() file.Idx    { return self.Throw }
-func (self *TryStatement) Idx1() file.Idx      { return self.Try }
-func (self *VariableStatement) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() }
-func (self *WhileStatement) Idx1() file.Idx    { return self.Body.Idx1() }
-func (self *WithStatement) Idx1() file.Idx     { return self.Body.Idx1() }
+func (self *LabelledStatement) Idx1() file.Idx   { return self.Colon + 1 }
+func (self *Program) Idx1() file.Idx             { return self.Body[len(self.Body)-1].Idx1() }
+func (self *ReturnStatement) Idx1() file.Idx     { return self.Return }
+func (self *SwitchStatement) Idx1() file.Idx     { return self.Body[len(self.Body)-1].Idx1() }
+func (self *ThrowStatement) Idx1() file.Idx      { return self.Throw }
+func (self *TryStatement) Idx1() file.Idx        { return self.Try }
+func (self *VariableStatement) Idx1() file.Idx   { return self.List[len(self.List)-1].Idx1() }
+func (self *WhileStatement) Idx1() file.Idx      { return self.Body.Idx1() }
+func (self *WithStatement) Idx1() file.Idx       { return self.Body.Idx1() }
+func (self *LexicalDeclaration) Idx1() file.Idx  { return self.List[len(self.List)-1].Idx1() }
+func (self *FunctionDeclaration) Idx1() file.Idx { return self.Function.Idx1() }
+
+func (self *ForLoopInitializerVarDeclList) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() }

+ 3 - 4
builtin_array.go

@@ -1292,11 +1292,9 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl {
 	o._putProp("toLocaleString", r.newNativeFunc(r.arrayproto_toLocaleString, nil, "toLocaleString", nil, 0), true, false, true)
 	o._putProp("toString", r.global.arrayToString, true, false, true)
 	o._putProp("unshift", r.newNativeFunc(r.arrayproto_unshift, nil, "unshift", nil, 1), true, false, true)
-	valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0)
-	r.global.arrayValues = valuesFunc
-	o._putProp("values", valuesFunc, true, false, true)
+	o._putProp("values", r.global.arrayValues, true, false, true)
 
-	o._putSym(SymIterator, valueProp(valuesFunc, true, false, true))
+	o._putSym(SymIterator, valueProp(r.global.arrayValues, true, false, true))
 
 	bl := r.newBaseObject(nil, classObject)
 	bl.setOwnStr("copyWithin", valueTrue, true)
@@ -1338,6 +1336,7 @@ func (r *Runtime) createArrayIterProto(val *Object) objectImpl {
 }
 
 func (r *Runtime) initArray() {
+	r.global.arrayValues = r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0)
 	r.global.arrayToString = r.newNativeFunc(r.arrayproto_toString, nil, "toString", nil, 0)
 
 	r.global.ArrayIteratorPrototype = r.newLazyObject(r.createArrayIterProto)

+ 2 - 2
builtin_regexp.go

@@ -765,7 +765,7 @@ func (r *Runtime) regexpproto_stdMatcherAll(call FunctionCall) Value {
 	flags := nilSafe(thisObj.self.getStr("flags", nil)).toString()
 	c := r.speciesConstructorObj(call.This.(*Object), r.global.RegExp)
 	matcher := r.toConstructor(c)([]Value{call.This, flags}, nil)
-	matcher.self.setOwnStr("lastIndex", valueInt(toLength(thisObj.Get("lastIndex"))), true)
+	matcher.self.setOwnStr("lastIndex", valueInt(toLength(thisObj.self.getStr("lastIndex", nil))), true)
 	flagsStr := flags.String()
 	global := strings.Contains(flagsStr, "g")
 	fullUnicode := strings.Contains(flagsStr, "u")
@@ -800,7 +800,7 @@ type regExpStringIterObject struct {
 
 // RegExpExec as defined in 21.2.5.2.1
 func regExpExec(r *Object, s valueString) Value {
-	exec := r.Get("exec")
+	exec := r.self.getStr("exec", nil)
 	if execObject, ok := exec.(*Object); ok {
 		if execFn, ok := execObject.self.assertCallable(); ok {
 			return r.runtime.regExpExec(execFn, r, s)

+ 1 - 1
builtin_string.go

@@ -377,7 +377,7 @@ func (r *Runtime) stringproto_matchAll(call FunctionCall) Value {
 	if regexp != _undefined && regexp != _null {
 		if isRegexp(regexp) {
 			if o, ok := regexp.(*Object); ok {
-				flags := o.Get("flags")
+				flags := o.self.getStr("flags", nil)
 				r.checkObjectCoercible(flags)
 				if !strings.Contains(flags.toString().String(), "g") {
 					panic(r.NewTypeError("RegExp doesn't have global flag set"))

+ 604 - 192
compiler.go

@@ -3,20 +3,39 @@ package goja
 import (
 	"fmt"
 	"sort"
-	"strconv"
 
 	"github.com/dop251/goja/ast"
 	"github.com/dop251/goja/file"
 	"github.com/dop251/goja/unistring"
 )
 
+type blockType int
+
 const (
-	blockLoop = iota
+	blockLoop blockType = iota
 	blockLoopEnum
 	blockTry
-	blockBranch
+	blockLabel
 	blockSwitch
 	blockWith
+	blockScope
+	blockIterScope
+)
+
+const (
+	maskConst     = 1 << 31
+	maskVar       = 1 << 30
+	maskDeletable = maskConst
+
+	maskTyp = maskConst | maskVar
+)
+
+type varType byte
+
+const (
+	varTypeVar varType = iota
+	varTypeLet
+	varTypeConst
 )
 
 type CompilerError struct {
@@ -48,39 +67,188 @@ type Program struct {
 }
 
 type compiler struct {
-	p          *Program
-	scope      *scope
-	block      *block
-	blockStart int
+	p     *Program
+	scope *scope
+	block *block
 
 	enumGetExpr compiledEnumGetExpr
 
 	evalVM *vm
 }
 
+type binding struct {
+	scope        *scope
+	name         unistring.String
+	accessPoints map[*scope]*[]int
+	isConst      bool
+	isArg        bool
+	isVar        bool
+	inStash      bool
+}
+
+func (b *binding) getAccessPointsForScope(s *scope) *[]int {
+	m := b.accessPoints[s]
+	if m == nil {
+		a := make([]int, 0, 1)
+		m = &a
+		if b.accessPoints == nil {
+			b.accessPoints = make(map[*scope]*[]int)
+		}
+		b.accessPoints[s] = m
+	}
+	return m
+}
+
+func (b *binding) markAccessPoint() {
+	scope := b.scope.c.scope
+	m := b.getAccessPointsForScope(scope)
+	*m = append(*m, len(scope.prg.code)-scope.base)
+}
+
+func (b *binding) emitGet() {
+	b.markAccessPoint()
+	if b.isVar && !b.isArg {
+		b.scope.c.emit(loadStash(0))
+	} else {
+		b.scope.c.emit(loadStashLex(0))
+	}
+}
+
+func (b *binding) emitGetP() {
+	if b.isVar && !b.isArg {
+		// no-op
+	} else {
+		// make sure TDZ is checked
+		b.markAccessPoint()
+		b.scope.c.emit(loadStashLex(0), pop)
+	}
+}
+
+func (b *binding) emitSet() {
+	if b.isConst {
+		b.scope.c.emit(throwAssignToConst)
+		return
+	}
+	b.markAccessPoint()
+	if b.isVar && !b.isArg {
+		b.scope.c.emit(storeStash(0))
+	} else {
+		b.scope.c.emit(storeStashLex(0))
+	}
+}
+
+func (b *binding) emitSetP() {
+	if b.isConst {
+		b.scope.c.emit(throwAssignToConst)
+		return
+	}
+	b.markAccessPoint()
+	if b.isVar && !b.isArg {
+		b.scope.c.emit(storeStashP(0))
+	} else {
+		b.scope.c.emit(storeStashLexP(0))
+	}
+}
+
+func (b *binding) emitInit() {
+	b.markAccessPoint()
+	b.scope.c.emit(initStash(0))
+}
+
+func (b *binding) emitGetVar(callee bool) {
+	b.markAccessPoint()
+	if b.isVar && !b.isArg {
+		b.scope.c.emit(&loadMixed{name: b.name, callee: callee})
+	} else {
+		b.scope.c.emit(&loadMixedLex{name: b.name, callee: callee})
+	}
+}
+
+func (b *binding) emitResolveVar(strict bool) {
+	b.markAccessPoint()
+	if b.isVar && !b.isArg {
+		b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: varTypeVar})
+	} else {
+		var typ varType
+		if b.isConst {
+			typ = varTypeConst
+		} else {
+			typ = varTypeLet
+		}
+		b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: typ})
+	}
+}
+
+func (b *binding) moveToStash() {
+	if b.isArg && !b.scope.argsInStash {
+		b.scope.moveArgsToStash()
+	} else {
+		b.inStash = true
+		b.scope.needStash = true
+	}
+}
+
+func (b *binding) useCount() (count int) {
+	for _, a := range b.accessPoints {
+		count += len(*a)
+	}
+	return
+}
+
 type scope struct {
-	names      map[unistring.String]uint32
+	c          *compiler
+	prg        *Program
 	outer      *scope
-	strict     bool
-	eval       bool
-	lexical    bool
-	dynamic    bool
-	accessed   bool
+	nested     []*scope
+	boundNames map[unistring.String]*binding
+	bindings   []*binding
+	base       int
+	numArgs    int
+
+	// in strict mode
+	strict bool
+	// eval top-level scope
+	eval bool
+	// at least one inner scope has direct eval() which can lookup names dynamically (by name)
+	dynLookup bool
+	// at least one binding has been marked for placement in stash
+	needStash bool
+
+	// is a function or a top-level lexical environment
+	function bool
+	// a function scope that has at least one direct eval() and non-strict, so the variables can be added dynamically
+	dynamic bool
+	// arguments have been marked for placement in stash (functions only)
+	argsInStash bool
+	// need 'arguments' object (functions only)
 	argsNeeded bool
+	// 'this' is used and non-strict, so need to box it (functions only)
 	thisNeeded bool
-
-	namesMap    map[unistring.String]unistring.String
-	lastFreeTmp int
 }
 
 type block struct {
-	typ        int
+	typ        blockType
 	label      unistring.String
-	needResult bool
 	cont       int
 	breaks     []int
 	conts      []int
 	outer      *block
+	breaking   *block // set when the 'finally' block is an empty break statement sequence
+	needResult bool
+}
+
+func (c *compiler) leaveScopeBlock(enter *enterBlock) {
+	c.updateEnterBlock(enter)
+	leave := &leaveBlock{
+		stackSize: enter.stackSize,
+		popStash:  enter.stashSize > 0,
+	}
+	c.emit(leave)
+	for _, pc := range c.block.breaks {
+		c.p.code[pc] = leave
+	}
+	c.block.breaks = nil
+	c.leaveBlock()
 }
 
 func (c *compiler) leaveBlock() {
@@ -113,13 +281,21 @@ func (c *compiler) newScope() {
 		strict = c.scope.strict
 	}
 	c.scope = &scope{
-		outer:    c.scope,
-		names:    make(map[unistring.String]uint32),
-		strict:   strict,
-		namesMap: make(map[unistring.String]unistring.String),
+		c:      c,
+		prg:    c.p,
+		outer:  c.scope,
+		strict: strict,
 	}
 }
 
+func (c *compiler) newBlockScope() {
+	c.newScope()
+	if outer := c.scope.outer; outer != nil {
+		outer.nested = append(outer.nested, c.scope)
+	}
+	c.scope.base = len(c.p.code)
+}
+
 func (c *compiler) popScope() {
 	c.scope = c.scope.outer
 }
@@ -131,8 +307,6 @@ func newCompiler() *compiler {
 
 	c.enumGetExpr.init(c, file.Idx(0))
 
-	c.newScope()
-	c.scope.dynamic = true
 	return c
 }
 
@@ -172,243 +346,478 @@ func (p *Program) sourceOffset(pc int) int {
 	return 0
 }
 
-func (s *scope) isFunction() bool {
-	if !s.lexical {
-		return s.outer != nil
-	}
-	return s.outer.isFunction()
-}
-
-func (s *scope) lookupName(name unistring.String) (idx uint32, found, noDynamics bool) {
-	var level uint32 = 0
+func (s *scope) lookupName(name unistring.String) (binding *binding, noDynamics bool) {
 	noDynamics = true
+	toStash := false
 	for curScope := s; curScope != nil; curScope = curScope.outer {
-		if curScope != s {
-			curScope.accessed = true
-		}
 		if curScope.dynamic {
 			noDynamics = false
 		} else {
-			var mapped unistring.String
-			if m, exists := curScope.namesMap[name]; exists {
-				mapped = m
-			} else {
-				mapped = name
-			}
-			if i, exists := curScope.names[mapped]; exists {
-				idx = i | (level << 24)
-				found = true
+			if b, exists := curScope.boundNames[name]; exists {
+				if toStash && !b.inStash {
+					b.moveToStash()
+				}
+				binding = b
 				return
 			}
 		}
-		if name == "arguments" && !s.lexical && s.isFunction() {
-			s.argsNeeded = true
-			s.accessed = true
-			idx, _ = s.bindName(name)
-			found = true
+		if name == "arguments" && curScope.function {
+			curScope.argsNeeded = true
+			binding, _ = curScope.bindName(name)
 			return
 		}
-		level++
+		if curScope.function {
+			toStash = true
+		}
 	}
 	return
 }
 
-func (s *scope) bindName(name unistring.String) (uint32, bool) {
-	if s.lexical {
-		return s.outer.bindName(name)
+func (s *scope) ensureBoundNamesCreated() {
+	if s.boundNames == nil {
+		s.boundNames = make(map[unistring.String]*binding)
 	}
+}
 
-	if idx, exists := s.names[name]; exists {
-		return idx, false
+func (s *scope) bindNameLexical(name unistring.String, unique bool, offset int) (*binding, bool) {
+	if b := s.boundNames[name]; b != nil {
+		if unique {
+			s.c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name)
+		}
+		return b, false
+	}
+	if len(s.bindings) >= (1<<24)-1 {
+		s.c.throwSyntaxError(offset, "Too many variables")
+	}
+	b := &binding{
+		scope: s,
+		name:  name,
 	}
-	idx := uint32(len(s.names))
-	s.names[name] = idx
-	return idx, true
+	s.bindings = append(s.bindings, b)
+	s.ensureBoundNamesCreated()
+	s.boundNames[name] = b
+	return b, true
 }
 
-func (s *scope) bindNameShadow(name unistring.String) (uint32, bool) {
-	if s.lexical {
+func (s *scope) bindName(name unistring.String) (*binding, bool) {
+	if !s.function && s.outer != nil {
 		return s.outer.bindName(name)
 	}
+	b, created := s.bindNameLexical(name, false, 0)
+	if created {
+		b.isVar = true
+	}
+	return b, created
+}
 
-	unique := true
+func (s *scope) bindNameShadow(name unistring.String) (*binding, bool) {
+	if !s.function && s.outer != nil {
+		return s.outer.bindNameShadow(name)
+	}
 
-	if idx, exists := s.names[name]; exists {
-		unique = false
-		// shadow the var
-		delete(s.names, name)
-		n := unistring.String(strconv.Itoa(int(idx)))
-		s.names[n] = idx
+	_, exists := s.boundNames[name]
+	b := &binding{
+		scope: s,
+		name:  name,
 	}
-	idx := uint32(len(s.names))
-	s.names[name] = idx
-	return idx, unique
+	s.bindings = append(s.bindings, b)
+	s.ensureBoundNamesCreated()
+	s.boundNames[name] = b
+	return b, !exists
 }
 
-func (c *compiler) markBlockStart() {
-	c.blockStart = len(c.p.code)
+func (s *scope) nearestFunction() *scope {
+	for sc := s; sc != nil; sc = sc.outer {
+		if sc.function {
+			return sc
+		}
+	}
+	return nil
 }
 
-func (c *compiler) compile(in *ast.Program) {
-	c.p.src = in.File
+func (s *scope) finaliseVarAlloc(stackOffset int) (stashSize, stackSize int) {
+	argsInStash := false
+	if f := s.nearestFunction(); f != nil {
+		argsInStash = f.argsInStash
+	}
+	stackIdx, stashIdx := 0, 0
+	allInStash := s.isDynamic()
+	for i, b := range s.bindings {
+		if allInStash || b.inStash {
+			for scope, aps := range b.accessPoints {
+				var level uint32
+				for sc := scope; sc != nil && sc != s; sc = sc.outer {
+					if sc.needStash || sc.isDynamic() {
+						level++
+					}
+				}
+				if level > 255 {
+					s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded")
+				}
+				idx := (level << 24) | uint32(stashIdx)
+				base := scope.base
+				code := scope.prg.code
+				for _, pc := range *aps {
+					ap := &code[base+pc]
+					switch i := (*ap).(type) {
+					case loadStash:
+						*ap = loadStash(idx)
+					case storeStash:
+						*ap = storeStash(idx)
+					case storeStashP:
+						*ap = storeStashP(idx)
+					case loadStashLex:
+						*ap = loadStashLex(idx)
+					case storeStashLex:
+						*ap = storeStashLex(idx)
+					case storeStashLexP:
+						*ap = storeStashLexP(idx)
+					case initStash:
+						*ap = initStash(idx)
+					case *loadMixed:
+						i.idx = idx
+					case *resolveMixed:
+						i.idx = idx
+					}
+				}
+			}
+			stashIdx++
+		} else {
+			var idx int
+			if i < s.numArgs {
+				idx = -(i + 1)
+			} else {
+				stackIdx++
+				idx = stackIdx + stackOffset
+			}
+			for scope, aps := range b.accessPoints {
+				var level int
+				for sc := scope; sc != nil && sc != s; sc = sc.outer {
+					if sc.needStash || sc.isDynamic() {
+						level++
+					}
+				}
+				if level > 255 {
+					s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded")
+				}
+				code := scope.prg.code
+				base := scope.base
+				if argsInStash {
+					for _, pc := range *aps {
+						ap := &code[base+pc]
+						switch i := (*ap).(type) {
+						case loadStash:
+							*ap = loadStack1(idx)
+						case storeStash:
+							*ap = storeStack1(idx)
+						case storeStashP:
+							*ap = storeStack1P(idx)
+						case loadStashLex:
+							*ap = loadStack1Lex(idx)
+						case storeStashLex:
+							*ap = storeStack1Lex(idx)
+						case storeStashLexP:
+							*ap = storeStack1LexP(idx)
+						case initStash:
+							*ap = initStack1(idx)
+						case *loadMixed:
+							*ap = &loadMixedStack1{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
+						case *loadMixedLex:
+							*ap = &loadMixedStack1Lex{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
+						case *resolveMixed:
+							*ap = &resolveMixedStack1{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict}
+						}
+					}
+				} else {
+					for _, pc := range *aps {
+						ap := &code[base+pc]
+						switch i := (*ap).(type) {
+						case loadStash:
+							*ap = loadStack(idx)
+						case storeStash:
+							*ap = storeStack(idx)
+						case storeStashP:
+							*ap = storeStackP(idx)
+						case loadStashLex:
+							*ap = loadStackLex(idx)
+						case storeStashLex:
+							*ap = storeStackLex(idx)
+						case storeStashLexP:
+							*ap = storeStackLexP(idx)
+						case initStash:
+							*ap = initStack(idx)
+						case *loadMixed:
+							*ap = &loadMixedStack{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
+						case *loadMixedLex:
+							*ap = &loadMixedStackLex{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
+						case *resolveMixed:
+							*ap = &resolveMixedStack{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict}
+						}
+					}
+				}
+			}
+		}
+	}
+	for _, nested := range s.nested {
+		nested.finaliseVarAlloc(stackIdx + stackOffset)
+	}
+	return stashIdx, stackIdx
+}
 
-	if len(in.Body) > 0 {
-		if !c.scope.strict {
-			c.scope.strict = c.isStrict(in.Body)
+func (s *scope) moveArgsToStash() {
+	for _, b := range s.bindings {
+		if !b.isArg {
+			break
 		}
+		b.inStash = true
 	}
+	s.argsInStash = true
+	s.needStash = true
+}
 
-	c.compileDeclList(in.DeclarationList, false)
-	c.compileFunctions(in.DeclarationList)
+func (s *scope) adjustBase(delta int) {
+	s.base += delta
+	for _, nested := range s.nested {
+		nested.adjustBase(delta)
+	}
+}
 
-	c.markBlockStart()
-	c.compileStatements(in.Body, true)
+func (s *scope) makeNamesMap() map[unistring.String]uint32 {
+	l := len(s.bindings)
+	if l == 0 {
+		return nil
+	}
+	names := make(map[unistring.String]uint32, l)
+	for i, b := range s.bindings {
+		idx := uint32(i)
+		if b.isConst {
+			idx |= maskConst
+		}
+		if b.isVar {
+			idx |= maskVar
+		}
+		names[b.name] = idx
+	}
+	return names
+}
 
-	c.p.code = append(c.p.code, halt)
-	code := c.p.code
-	c.p.code = make([]instruction, 0, len(code)+len(c.scope.names)+2)
-	if c.scope.eval {
-		if !c.scope.strict {
-			c.emit(jne(2), newStash)
+func (s *scope) isDynamic() bool {
+	return s.dynLookup || s.dynamic
+}
+
+func (s *scope) deleteBinding(b *binding) {
+	idx := 0
+	for i, bb := range s.bindings {
+		if bb == b {
+			idx = i
+			goto found
+		}
+	}
+	return
+found:
+	delete(s.boundNames, b.name)
+	copy(s.bindings[idx:], s.bindings[idx+1:])
+	l := len(s.bindings) - 1
+	s.bindings[l] = nil
+	s.bindings = s.bindings[:l]
+}
+
+func (c *compiler) compile(in *ast.Program, strict, eval, inGlobal bool) {
+	c.p.src = in.File
+	c.newScope()
+	scope := c.scope
+	scope.dynamic = true
+	scope.eval = eval
+	if !strict && len(in.Body) > 0 {
+		strict = c.isStrict(in.Body)
+	}
+	scope.strict = strict
+	ownVarScope := eval && strict
+	ownLexScope := !inGlobal || eval
+	if ownVarScope {
+		c.newBlockScope()
+		scope = c.scope
+		scope.function = true
+	}
+	funcs := c.extractFunctions(in.Body)
+	c.createFunctionBindings(funcs)
+	numFuncs := len(scope.bindings)
+	if inGlobal && !ownVarScope {
+		if numFuncs == len(funcs) {
+			c.compileFunctionsGlobalAllUnique(funcs)
+		} else {
+			c.compileFunctionsGlobal(funcs)
+		}
+	}
+	c.compileDeclList(in.DeclarationList, false)
+	numVars := len(scope.bindings) - numFuncs
+	vars := make([]unistring.String, len(scope.bindings))
+	for i, b := range scope.bindings {
+		vars[i] = b.name
+	}
+	if len(vars) > 0 && !ownVarScope && ownLexScope {
+		if inGlobal {
+			c.emit(&bindGlobal{
+				vars:      vars[numFuncs:],
+				funcs:     vars[:numFuncs],
+				deletable: eval,
+			})
 		} else {
-			c.emit(pop, newStash)
+			c.emit(&bindVars{names: vars, deletable: eval})
 		}
 	}
-	l := len(c.p.code)
-	c.p.code = c.p.code[:l+len(c.scope.names)]
-	for name, nameIdx := range c.scope.names {
-		c.p.code[l+int(nameIdx)] = bindName(name)
+	var enter *enterBlock
+	if c.compileLexicalDeclarations(in.Body, ownVarScope || !ownLexScope) {
+		if ownLexScope {
+			c.block = &block{
+				outer:      c.block,
+				typ:        blockScope,
+				needResult: true,
+			}
+			enter = &enterBlock{}
+			c.emit(enter)
+		}
 	}
-
-	c.p.code = append(c.p.code, code...)
-	for i := range c.p.srcMap {
-		c.p.srcMap[i].pc += len(c.scope.names)
+	if len(scope.bindings) > 0 && !ownLexScope {
+		var lets, consts []unistring.String
+		for _, b := range c.scope.bindings[numFuncs+numVars:] {
+			if b.isConst {
+				consts = append(consts, b.name)
+			} else {
+				lets = append(lets, b.name)
+			}
+		}
+		c.emit(&bindGlobal{
+			vars:   vars[numFuncs:],
+			funcs:  vars[:numFuncs],
+			lets:   lets,
+			consts: consts,
+		})
+	}
+	if !inGlobal || ownVarScope {
+		c.compileFunctions(funcs)
+	}
+	c.compileStatements(in.Body, true)
+	if enter != nil {
+		c.leaveScopeBlock(enter)
+		c.popScope()
 	}
 
+	c.p.code = append(c.p.code, halt)
+
+	scope.finaliseVarAlloc(0)
 }
 
-func (c *compiler) compileDeclList(v []ast.Declaration, inFunc bool) {
+func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) {
 	for _, value := range v {
-		switch value := value.(type) {
+		c.compileVarDecl(value, inFunc)
+	}
+}
+
+func (c *compiler) extractLabelled(st ast.Statement) ast.Statement {
+	if st, ok := st.(*ast.LabelledStatement); ok {
+		return c.extractLabelled(st.Statement)
+	}
+	return st
+}
+
+func (c *compiler) extractFunctions(list []ast.Statement) (funcs []*ast.FunctionDeclaration) {
+	for _, st := range list {
+		var decl *ast.FunctionDeclaration
+		switch st := c.extractLabelled(st).(type) {
 		case *ast.FunctionDeclaration:
-			c.compileFunctionDecl(value)
-		case *ast.VariableDeclaration:
-			c.compileVarDecl(value, inFunc)
+			decl = st
+		case *ast.LabelledStatement:
+			if st1, ok := st.Statement.(*ast.FunctionDeclaration); ok {
+				decl = st1
+			} else {
+				continue
+			}
 		default:
-			panic(fmt.Errorf("Unsupported declaration: %T", value))
+			continue
 		}
+		funcs = append(funcs, decl)
 	}
+	return
 }
 
-func (c *compiler) compileFunctions(v []ast.Declaration) {
-	for _, value := range v {
-		if value, ok := value.(*ast.FunctionDeclaration); ok {
-			c.compileFunction(value)
+func (c *compiler) createFunctionBindings(funcs []*ast.FunctionDeclaration) {
+	s := c.scope
+	if s.outer != nil {
+		unique := !s.function && s.strict
+		for _, decl := range funcs {
+			s.bindNameLexical(decl.Function.Name.Name, unique, int(decl.Function.Name.Idx1())-1)
+		}
+	} else {
+		for _, decl := range funcs {
+			s.bindName(decl.Function.Name.Name)
 		}
 	}
 }
 
-func (c *compiler) compileVarDecl(v *ast.VariableDeclaration, inFunc bool) {
-	for _, item := range v.List {
-		if c.scope.strict {
-			c.checkIdentifierLName(item.Name, int(item.Idx)-1)
-			c.checkIdentifierName(item.Name, int(item.Idx)-1)
-		}
-		if !inFunc || item.Name != "arguments" {
-			idx, ok := c.scope.bindName(item.Name)
-			_ = idx
-			//log.Printf("Define var: %s: %x", item.Name, idx)
-			if !ok {
-				// TODO: error
-			}
-		}
+func (c *compiler) compileFunctions(list []*ast.FunctionDeclaration) {
+	for _, decl := range list {
+		c.compileFunction(decl)
 	}
 }
 
-func (c *compiler) addDecls() []instruction {
-	code := make([]instruction, len(c.scope.names))
-	for name, nameIdx := range c.scope.names {
-		code[nameIdx] = bindName(name)
+func (c *compiler) compileFunctionsGlobalAllUnique(list []*ast.FunctionDeclaration) {
+	for _, decl := range list {
+		c.compileFunctionLiteral(decl.Function, false).emitGetter(true)
 	}
-	return code
 }
 
-func (c *compiler) convertInstrToStashless(instr uint32, args int) (newIdx int, convert bool) {
-	level := instr >> 24
-	idx := instr & 0x00FFFFFF
-	if level > 0 {
-		level--
-		newIdx = int((level << 24) | idx)
-	} else {
-		iidx := int(idx)
-		if iidx < args {
-			newIdx = -iidx - 1
+func (c *compiler) compileFunctionsGlobal(list []*ast.FunctionDeclaration) {
+	m := make(map[unistring.String]int, len(list))
+	for i := len(list) - 1; i >= 0; i-- {
+		name := list[i].Function.Name.Name
+		if _, exists := m[name]; !exists {
+			m[name] = i
+		}
+	}
+	for i, decl := range list {
+		if m[decl.Function.Name.Name] == i {
+			c.compileFunctionLiteral(decl.Function, false).emitGetter(true)
 		} else {
-			newIdx = iidx - args + 1
+			leave := c.enterDummyMode()
+			c.compileFunctionLiteral(decl.Function, false).emitGetter(false)
+			leave()
 		}
-		convert = true
 	}
-	return
 }
 
-func (c *compiler) convertFunctionToStashless(code []instruction, args int) {
-	code[0] = enterFuncStashless{stackSize: uint32(len(c.scope.names) - args), args: uint32(args)}
-	for pc := 1; pc < len(code); pc++ {
-		instr := code[pc]
-		if instr == ret {
-			code[pc] = retStashless
+func (c *compiler) compileVarDecl(v *ast.VariableDeclaration, inFunc bool) {
+	for _, item := range v.List {
+		if c.scope.strict {
+			c.checkIdentifierLName(item.Name, int(item.Idx)-1)
+			c.checkIdentifierName(item.Name, int(item.Idx)-1)
 		}
-		switch instr := instr.(type) {
-		case getLocal:
-			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
-				code[pc] = loadStack(newIdx)
-			} else {
-				code[pc] = getLocal(newIdx)
-			}
-		case setLocal:
-			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
-				code[pc] = storeStack(newIdx)
-			} else {
-				code[pc] = setLocal(newIdx)
-			}
-		case setLocalP:
-			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
-				code[pc] = storeStackP(newIdx)
-			} else {
-				code[pc] = setLocalP(newIdx)
-			}
-		case getVar:
-			level := instr.idx >> 24
-			idx := instr.idx & 0x00FFFFFF
-			level--
-			instr.idx = level<<24 | idx
-			code[pc] = instr
-		case setVar:
-			level := instr.idx >> 24
-			idx := instr.idx & 0x00FFFFFF
-			level--
-			instr.idx = level<<24 | idx
-			code[pc] = instr
+		if !inFunc || item.Name != "arguments" {
+			c.scope.bindName(item.Name)
 		}
 	}
 }
 
-func (c *compiler) compileFunctionDecl(v *ast.FunctionDeclaration) {
-	idx, ok := c.scope.bindName(v.Function.Name.Name)
-	if !ok {
-		// TODO: error
+func (c *compiler) compileFunction(v *ast.FunctionDeclaration) {
+	name := v.Function.Name.Name
+	b := c.scope.boundNames[name]
+	if b == nil || b.isVar {
+		e := &compiledIdentifierExpr{
+			name: v.Function.Name.Name,
+		}
+		e.init(c, v.Function.Idx0())
+		e.emitSetter(c.compileFunctionLiteral(v.Function, false), false)
+	} else {
+		c.compileFunctionLiteral(v.Function, false).emitGetter(true)
+		b.emitInit()
 	}
-	_ = idx
-	// log.Printf("Define function: %s: %x", v.Function.Name.Name, idx)
 }
 
-func (c *compiler) compileFunction(v *ast.FunctionDeclaration) {
-	e := &compiledIdentifierExpr{
-		name: v.Function.Name.Name,
+func (c *compiler) compileStandaloneFunctionDecl(v *ast.FunctionDeclaration) {
+	if c.scope.strict {
+		c.throwSyntaxError(int(v.Idx0())-1, "In strict mode code, functions can only be declared at top level or inside a block.")
 	}
-	e.init(c, v.Function.Idx0())
-	e.emitSetter(c.compileFunctionLiteral(v.Function, false))
-	c.emit(pop)
+	c.throwSyntaxError(int(v.Idx0())-1, "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.")
 }
 
 func (c *compiler) emit(instructions ...instruction) {
@@ -468,18 +877,21 @@ func (c *compiler) checkIdentifierLName(name unistring.String, offset int) {
 // constant falsy condition 'if' branch or a loop (i.e 'if (false) { ... } or while (false) { ... }).
 // Such code should not be included in the final compilation result as it's never called, but it must
 // still produce compilation errors if there are any.
+// TODO: make sure variable lookups do not de-optimise parent scopes
 func (c *compiler) enterDummyMode() (leaveFunc func()) {
-	savedBlock, savedBlockStart, savedProgram := c.block, c.blockStart, c.p
+	savedBlock, savedProgram := c.block, c.p
 	if savedBlock != nil {
 		c.block = &block{
-			typ:   savedBlock.typ,
-			label: savedBlock.label,
+			typ:      savedBlock.typ,
+			label:    savedBlock.label,
+			outer:    savedBlock.outer,
+			breaking: savedBlock.breaking,
 		}
 	}
 	c.p = &Program{}
 	c.newScope()
 	return func() {
-		c.block, c.blockStart, c.p = savedBlock, savedBlockStart, savedProgram
+		c.block, c.p = savedBlock, savedProgram
 		c.popScope()
 	}
 }

+ 189 - 174
compiler_expr.go

@@ -16,7 +16,7 @@ var (
 
 type compiledExpr interface {
 	emitGetter(putOnStack bool)
-	emitSetter(valueExpr compiledExpr)
+	emitSetter(valueExpr compiledExpr, putOnStack bool)
 	emitUnary(prepare, body func(), postfix, putOnStack bool)
 	deleteExpr() compiledExpr
 	constant() bool
@@ -160,7 +160,6 @@ type compiledVariableExpr struct {
 	baseCompiledExpr
 	name        unistring.String
 	initializer compiledExpr
-	expr        *ast.VariableExpression
 }
 
 type compiledEnumGetExpr struct {
@@ -256,7 +255,7 @@ func (e *baseCompiledExpr) init(c *compiler, idx file.Idx) {
 	e.offset = int(idx) - 1
 }
 
-func (e *baseCompiledExpr) emitSetter(compiledExpr) {
+func (e *baseCompiledExpr) emitSetter(compiledExpr, bool) {
 	e.c.throwSyntaxError(e.offset, "Not a valid left-value expression")
 }
 
@@ -287,19 +286,21 @@ func (e *constantExpr) emitGetter(putOnStack bool) {
 
 func (e *compiledIdentifierExpr) emitGetter(putOnStack bool) {
 	e.addSrcMap()
-	if idx, found, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
-		if found {
+	if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
+		if b != nil {
 			if putOnStack {
-				e.c.emit(getLocal(idx))
+				b.emitGet()
+			} else {
+				b.emitGetP()
 			}
 		} else {
 			panic("No dynamics and not found")
 		}
 	} else {
-		if found {
-			e.c.emit(getVar{name: e.name, idx: idx})
+		if b != nil {
+			b.emitGetVar(false)
 		} else {
-			e.c.emit(getVar1(e.name))
+			e.c.emit(loadDynamic(e.name))
 		}
 		if !putOnStack {
 			e.c.emit(pop)
@@ -309,89 +310,98 @@ func (e *compiledIdentifierExpr) emitGetter(putOnStack bool) {
 
 func (e *compiledIdentifierExpr) emitGetterOrRef() {
 	e.addSrcMap()
-	if idx, found, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
-		if found {
-			e.c.emit(getLocal(idx))
+	if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
+		if b != nil {
+			b.emitGet()
 		} else {
 			panic("No dynamics and not found")
 		}
 	} else {
-		if found {
-			e.c.emit(getVar{name: e.name, idx: idx, ref: true})
+		if b != nil {
+			b.emitGetVar(false)
 		} else {
-			e.c.emit(getVar1Ref(e.name))
+			e.c.emit(loadDynamicRef(e.name))
 		}
 	}
 }
 
 func (e *compiledIdentifierExpr) emitGetterAndCallee() {
 	e.addSrcMap()
-	if idx, found, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
-		if found {
+	if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
+		if b != nil {
 			e.c.emit(loadUndef)
-			e.c.emit(getLocal(idx))
+			b.emitGet()
 		} else {
 			panic("No dynamics and not found")
 		}
 	} else {
-		if found {
-			e.c.emit(getVar{name: e.name, idx: idx, ref: true, callee: true})
+		if b != nil {
+			b.emitGetVar(true)
 		} else {
-			e.c.emit(getVar1Callee(e.name))
+			e.c.emit(loadDynamicCallee(e.name))
 		}
 	}
 }
 
-func (c *compiler) emitVarSetter1(name unistring.String, offset int, emitRight func(isRef bool)) {
+func (c *compiler) emitVarSetter1(name unistring.String, offset int, putOnStack bool, emitRight func(isRef bool)) {
 	if c.scope.strict {
 		c.checkIdentifierLName(name, offset)
 	}
 
-	if idx, found, noDynamics := c.scope.lookupName(name); noDynamics {
+	if b, noDynamics := c.scope.lookupName(name); noDynamics {
 		emitRight(false)
-		if found {
-			c.emit(setLocal(idx))
+		if b != nil {
+			if putOnStack {
+				b.emitSet()
+			} else {
+				b.emitSetP()
+			}
 		} else {
 			if c.scope.strict {
 				c.emit(setGlobalStrict(name))
 			} else {
 				c.emit(setGlobal(name))
 			}
+			if !putOnStack {
+				c.emit(pop)
+			}
 		}
 	} else {
-		if found {
-			c.emit(resolveVar{name: name, idx: idx, strict: c.scope.strict})
-			emitRight(true)
-			c.emit(putValue)
+		if b != nil {
+			b.emitResolveVar(c.scope.strict)
 		} else {
 			if c.scope.strict {
 				c.emit(resolveVar1Strict(name))
 			} else {
 				c.emit(resolveVar1(name))
 			}
-			emitRight(true)
+		}
+		emitRight(true)
+		if putOnStack {
 			c.emit(putValue)
+		} else {
+			c.emit(putValueP)
 		}
 	}
 }
 
-func (c *compiler) emitVarSetter(name unistring.String, offset int, valueExpr compiledExpr) {
-	c.emitVarSetter1(name, offset, func(bool) {
+func (c *compiler) emitVarSetter(name unistring.String, offset int, valueExpr compiledExpr, putOnStack bool) {
+	c.emitVarSetter1(name, offset, putOnStack, func(bool) {
 		c.emitExpr(valueExpr, true)
 	})
 }
 
-func (e *compiledVariableExpr) emitSetter(valueExpr compiledExpr) {
-	e.c.emitVarSetter(e.name, e.offset, valueExpr)
+func (e *compiledVariableExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) {
+	e.c.emitVarSetter(e.name, e.offset, valueExpr, putOnStack)
 }
 
-func (e *compiledIdentifierExpr) emitSetter(valueExpr compiledExpr) {
-	e.c.emitVarSetter(e.name, e.offset, valueExpr)
+func (e *compiledIdentifierExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) {
+	e.c.emitVarSetter(e.name, e.offset, valueExpr, putOnStack)
 }
 
 func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) {
 	if putOnStack {
-		e.c.emitVarSetter1(e.name, e.offset, func(isRef bool) {
+		e.c.emitVarSetter1(e.name, e.offset, true, func(isRef bool) {
 			e.c.emit(loadUndef)
 			if isRef {
 				e.c.emit(getValue)
@@ -411,7 +421,7 @@ func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnS
 		})
 		e.c.emit(pop)
 	} else {
-		e.c.emitVarSetter1(e.name, e.offset, func(isRef bool) {
+		e.c.emitVarSetter1(e.name, e.offset, false, func(isRef bool) {
 			if isRef {
 				e.c.emit(getValue)
 			} else {
@@ -419,7 +429,6 @@ func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnS
 			}
 			body()
 		})
-		e.c.emit(pop)
 	}
 }
 
@@ -428,27 +437,28 @@ func (e *compiledIdentifierExpr) deleteExpr() compiledExpr {
 		e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode")
 		panic("Unreachable")
 	}
-	if _, found, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
-		if !found {
+	if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics {
+		if b == nil {
 			r := &deleteGlobalExpr{
 				name: e.name,
 			}
 			r.init(e.c, file.Idx(0))
 			return r
-		} else {
-			r := &constantExpr{
-				val: valueFalse,
-			}
-			r.init(e.c, file.Idx(0))
-			return r
 		}
 	} else {
-		r := &deleteVarExpr{
-			name: e.name,
+		if b == nil {
+			r := &deleteVarExpr{
+				name: e.name,
+			}
+			r.init(e.c, file.Idx(e.offset+1))
+			return r
 		}
-		r.init(e.c, file.Idx(e.offset+1))
-		return r
 	}
+	r := &compiledLiteral{
+		val: valueFalse,
+	}
+	r.init(e.c, file.Idx(e.offset+1))
+	return r
 }
 
 type compiledDotExpr struct {
@@ -466,13 +476,21 @@ func (e *compiledDotExpr) emitGetter(putOnStack bool) {
 	}
 }
 
-func (e *compiledDotExpr) emitSetter(valueExpr compiledExpr) {
+func (e *compiledDotExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) {
 	e.left.emitGetter(true)
 	valueExpr.emitGetter(true)
 	if e.c.scope.strict {
-		e.c.emit(setPropStrict(e.name))
+		if putOnStack {
+			e.c.emit(setPropStrict(e.name))
+		} else {
+			e.c.emit(setPropStrictP(e.name))
+		}
 	} else {
-		e.c.emit(setProp(e.name))
+		if putOnStack {
+			e.c.emit(setProp(e.name))
+		} else {
+			e.c.emit(setPropP(e.name))
+		}
 	}
 }
 
@@ -540,14 +558,22 @@ func (e *compiledBracketExpr) emitGetter(putOnStack bool) {
 	}
 }
 
-func (e *compiledBracketExpr) emitSetter(valueExpr compiledExpr) {
+func (e *compiledBracketExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) {
 	e.left.emitGetter(true)
 	e.member.emitGetter(true)
 	valueExpr.emitGetter(true)
 	if e.c.scope.strict {
-		e.c.emit(setElemStrict)
+		if putOnStack {
+			e.c.emit(setElemStrict)
+		} else {
+			e.c.emit(setElemStrictP)
+		}
 	} else {
-		e.c.emit(setElem)
+		if putOnStack {
+			e.c.emit(setElem)
+		} else {
+			e.c.emit(setElemP)
+		}
 	}
 }
 
@@ -668,79 +694,65 @@ func (e *compiledAssignExpr) emitGetter(putOnStack bool) {
 				}
 			}
 		}
-		e.left.emitSetter(e.right)
+		e.left.emitSetter(e.right, putOnStack)
 	case token.PLUS:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(add)
 		}, false, putOnStack)
-		return
 	case token.MINUS:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(sub)
 		}, false, putOnStack)
-		return
 	case token.MULTIPLY:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(mul)
 		}, false, putOnStack)
-		return
 	case token.SLASH:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(div)
 		}, false, putOnStack)
-		return
 	case token.REMAINDER:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(mod)
 		}, false, putOnStack)
-		return
 	case token.OR:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(or)
 		}, false, putOnStack)
-		return
 	case token.AND:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(and)
 		}, false, putOnStack)
-		return
 	case token.EXCLUSIVE_OR:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(xor)
 		}, false, putOnStack)
-		return
 	case token.SHIFT_LEFT:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(sal)
 		}, false, putOnStack)
-		return
 	case token.SHIFT_RIGHT:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(sar)
 		}, false, putOnStack)
-		return
 	case token.UNSIGNED_SHIFT_RIGHT:
 		e.left.emitUnary(nil, func() {
 			e.right.emitGetter(true)
 			e.c.emit(shr)
 		}, false, putOnStack)
-		return
 	default:
 		panic(fmt.Errorf("Unknown assign operator: %s", e.operator.String()))
 	}
-	if !putOnStack {
-		e.c.emit(pop)
-	}
 }
 
 func (e *compiledLiteral) emitGetter(putOnStack bool) {
@@ -755,13 +767,12 @@ func (e *compiledLiteral) constant() bool {
 }
 
 func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) {
-	e.c.newScope()
-	savedBlockStart := e.c.blockStart
 	savedPrg := e.c.p
 	e.c.p = &Program{
 		src: e.c.p.src,
 	}
-	e.c.blockStart = 0
+	e.c.newScope()
+	e.c.scope.function = true
 
 	var name unistring.String
 	if e.expr.Name != nil {
@@ -773,12 +784,15 @@ func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) {
 	if name != "" {
 		e.c.p.funcName = name
 	}
-	block := e.c.block
-	e.c.block = nil
+	savedBlock := e.c.block
 	defer func() {
-		e.c.block = block
+		e.c.block = savedBlock
 	}()
 
+	e.c.block = &block{
+		typ: blockScope,
+	}
+
 	if !e.c.scope.strict {
 		e.c.scope.strict = e.strict
 	}
@@ -793,107 +807,119 @@ func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) {
 	length := len(e.expr.ParameterList.List)
 
 	for _, item := range e.expr.ParameterList.List {
-		_, unique := e.c.scope.bindNameShadow(item.Name)
+		b, unique := e.c.scope.bindNameShadow(item.Name)
 		if !unique && e.c.scope.strict {
 			e.c.throwSyntaxError(int(item.Idx)-1, "Strict mode function may not have duplicate parameter names (%s)", item.Name)
 			return
 		}
+		b.isArg = true
+		b.isVar = true
 	}
-	paramsCount := len(e.c.scope.names)
+	paramsCount := len(e.c.scope.bindings)
+	e.c.scope.numArgs = paramsCount
 	e.c.compileDeclList(e.expr.DeclarationList, true)
-	var needCallee bool
-	var calleeIdx uint32
+	body := e.expr.Body.List
+	funcs := e.c.extractFunctions(body)
+	e.c.createFunctionBindings(funcs)
+	s := e.c.scope
+	e.c.compileLexicalDeclarations(body, true)
+	var calleeBinding *binding
 	if e.isExpr && e.expr.Name != nil {
-		if idx, ok := e.c.scope.bindName(e.expr.Name.Name); ok {
-			calleeIdx = idx
-			needCallee = true
+		if b, created := s.bindName(e.expr.Name.Name); created {
+			calleeBinding = b
 		}
 	}
-	maxPreambleLen := 2
-	e.c.p.code = make([]instruction, maxPreambleLen)
-	if needCallee {
-		e.c.emit(loadCallee, setLocalP(calleeIdx))
+	preambleLen := 4 // enter, boxThis, createArgs, set
+	e.c.p.code = make([]instruction, preambleLen, 8)
+
+	if calleeBinding != nil {
+		e.c.emit(loadCallee)
+		calleeBinding.emitSetP()
 	}
 
-	e.c.compileFunctions(e.expr.DeclarationList)
-	e.c.markBlockStart()
-	e.c.compileStatement(e.expr.Body, false)
+	e.c.compileFunctions(funcs)
+	e.c.compileStatements(body, false)
 
-	if e.c.blockStart >= len(e.c.p.code)-1 || e.c.p.code[len(e.c.p.code)-1] != ret {
+	var last ast.Statement
+	if l := len(body); l > 0 {
+		last = body[l-1]
+	}
+	if _, ok := last.(*ast.ReturnStatement); !ok {
 		e.c.emit(loadUndef, ret)
 	}
 
-	if !e.c.scope.dynamic && !e.c.scope.accessed {
-		// log.Printf("Function can use inline stash")
-		l := 0
-		if !e.c.scope.strict && e.c.scope.thisNeeded {
-			l = 2
-			e.c.p.code = e.c.p.code[maxPreambleLen-2:]
-			e.c.p.code[1] = boxThis
+	delta := 0
+	code := e.c.p.code
+
+	if calleeBinding != nil && !s.isDynamic() && calleeBinding.useCount() == 1 {
+		s.deleteBinding(calleeBinding)
+		preambleLen += 2
+	}
+
+	if s.argsNeeded {
+		s.moveArgsToStash()
+		pos := preambleLen - 2
+		delta += 2
+		if s.strict {
+			code[pos] = createArgsStrict(length)
 		} else {
-			l = 1
-			e.c.p.code = e.c.p.code[maxPreambleLen-1:]
-		}
-		e.c.convertFunctionToStashless(e.c.p.code, paramsCount)
-		for i := range e.c.p.srcMap {
-			e.c.p.srcMap[i].pc -= maxPreambleLen - l
-		}
-	} else {
-		l := 1 + len(e.c.scope.names)
-		if e.c.scope.argsNeeded {
-			l += 2
-		}
-		if !e.c.scope.strict && e.c.scope.thisNeeded {
-			l++
+			code[pos] = createArgs(length)
 		}
+		pos++
+		b, _ := s.bindName("arguments")
+		e.c.p.code = code[:pos]
+		b.emitSetP()
+		e.c.p.code = code
+	}
 
-		code := make([]instruction, l+len(e.c.p.code)-maxPreambleLen)
-		code[0] = enterFunc(length)
-		for name, nameIdx := range e.c.scope.names {
-			code[nameIdx+1] = bindName(name)
-		}
-		pos := 1 + len(e.c.scope.names)
+	stashSize, stackSize := s.finaliseVarAlloc(0)
 
-		if !e.c.scope.strict && e.c.scope.thisNeeded {
-			code[pos] = boxThis
-			pos++
+	if !s.strict && s.thisNeeded {
+		delta++
+		code[preambleLen-delta] = boxThis
+	}
+	delta++
+	delta = preambleLen - delta
+	var enter instruction
+	if stashSize > 0 || s.argsInStash || s.isDynamic() {
+		enter1 := enterFunc{
+			numArgs:     uint32(paramsCount),
+			argsToStash: s.argsInStash,
+			stashSize:   uint32(stashSize),
+			stackSize:   uint32(stackSize),
+			extensible:  s.dynamic,
 		}
-
-		if e.c.scope.argsNeeded {
-			if e.c.scope.strict {
-				code[pos] = createArgsStrict(length)
-			} else {
-				code[pos] = createArgs(length)
-			}
-			pos++
-			idx, exists := e.c.scope.names["arguments"]
-			if !exists {
-				panic("No arguments")
-			}
-			code[pos] = setLocalP(idx)
-			pos++
+		if s.isDynamic() {
+			enter1.names = s.makeNamesMap()
 		}
-
-		copy(code[l:], e.c.p.code[maxPreambleLen:])
-		e.c.p.code = code
+		enter = &enter1
+	} else {
+		enter = &enterFuncStashless{
+			stackSize: uint32(stackSize),
+			args:      uint32(paramsCount),
+		}
+	}
+	code[delta] = enter
+	if delta != 0 {
+		e.c.p.code = code[delta:]
 		for i := range e.c.p.srcMap {
-			e.c.p.srcMap[i].pc += l - maxPreambleLen
+			e.c.p.srcMap[i].pc -= delta
 		}
+		s.adjustBase(-delta)
 	}
 
-	strict := e.c.scope.strict
+	strict := s.strict
 	p := e.c.p
 	// e.c.p.dumpCode()
 	e.c.popScope()
 	e.c.p = savedPrg
-	e.c.blockStart = savedBlockStart
 	e.c.emit(&newFunc{prg: p, length: uint32(length), name: name, srcStart: uint32(e.expr.Idx0() - 1), srcEnd: uint32(e.expr.Idx1() - 1), strict: strict})
 	if !putOnStack {
 		e.c.emit(pop)
 	}
 }
 
-func (c *compiler) compileFunctionLiteral(v *ast.FunctionLiteral, isExpr bool) compiledExpr {
+func (c *compiler) compileFunctionLiteral(v *ast.FunctionLiteral, isExpr bool) *compiledFunctionLiteral {
 	strict := c.scope.strict || c.isStrictStatement(v.Body)
 	if v.Name != nil && strict {
 		c.checkIdentifierLName(v.Name.Name, int(v.Name.Idx)-1)
@@ -907,17 +933,15 @@ func (c *compiler) compileFunctionLiteral(v *ast.FunctionLiteral, isExpr bool) c
 	return r
 }
 
-func nearestNonLexical(s *scope) *scope {
-	for ; s != nil && s.lexical; s = s.outer {
-	}
-	return s
-}
-
 func (e *compiledThisExpr) emitGetter(putOnStack bool) {
 	if putOnStack {
 		e.addSrcMap()
-		if e.c.scope.eval || e.c.scope.isFunction() {
-			nearestNonLexical(e.c.scope).thisNeeded = true
+		scope := e.c.scope
+		for ; scope != nil && !scope.function && !scope.eval; scope = scope.outer {
+		}
+
+		if scope != nil {
+			scope.thisNeeded = true
 			e.c.emit(loadStack(0))
 		} else {
 			e.c.emit(loadGlobalObject)
@@ -925,16 +949,6 @@ func (e *compiledThisExpr) emitGetter(putOnStack bool) {
 	}
 }
 
-/*
-func (e *compiledThisExpr) deleteExpr() compiledExpr {
-	r := &compiledLiteral{
-		val: valueTrue,
-	}
-	r.init(e.c, 0)
-	return r
-}
-*/
-
 func (e *compiledNewExpr) emitGetter(putOnStack bool) {
 	e.callee.emitGetter(true)
 	for _, expr := range e.args {
@@ -1007,7 +1021,7 @@ func (c *compiler) emitThrow(v Value) {
 		t := nilSafe(o.self.getStr("name", nil)).toString().String()
 		switch t {
 		case "TypeError":
-			c.emit(getVar1(t))
+			c.emit(loadDynamic(t))
 			msg := o.self.getStr("message", nil)
 			if msg != nil {
 				c.emit(loadVal(c.p.defineLiteralValue(msg)))
@@ -1206,7 +1220,6 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) {
 		return
 	}
 	e.c.emitExpr(e.left, true)
-	e.c.markBlockStart()
 	j := len(e.c.p.code)
 	e.addSrcMap()
 	e.c.emit(nil)
@@ -1249,7 +1262,6 @@ func (e *compiledLogicalAnd) emitGetter(putOnStack bool) {
 		return
 	}
 	e.left.emitGetter(true)
-	e.c.markBlockStart()
 	j = len(e.c.p.code)
 	e.addSrcMap()
 	e.c.emit(nil)
@@ -1364,10 +1376,7 @@ func (e *compiledVariableExpr) emitGetter(putOnStack bool) {
 			name: e.name,
 		}
 		idExpr.init(e.c, file.Idx(0))
-		idExpr.emitSetter(e.initializer)
-		if !putOnStack {
-			e.c.emit(pop)
-		}
+		idExpr.emitSetter(e.initializer, putOnStack)
 	} else {
 		if putOnStack {
 			e.c.emit(loadUndef)
@@ -1511,12 +1520,18 @@ func (e *compiledCallExpr) emitGetter(putOnStack bool) {
 
 	e.addSrcMap()
 	if calleeName == "eval" {
-		e.c.scope.dynamic = true
-		e.c.scope.thisNeeded = true
-		if e.c.scope.lexical {
-			e.c.scope.outer.dynamic = true
+		foundFunc := false
+		for sc := e.c.scope; sc != nil; sc = sc.outer {
+			if !foundFunc && sc.function {
+				foundFunc = true
+				sc.thisNeeded, sc.argsNeeded = true, true
+				if !sc.strict {
+					sc.dynamic = true
+				}
+			}
+			sc.dynLookup = true
 		}
-		e.c.scope.accessed = true
+
 		if e.c.scope.strict {
 			e.c.emit(callEvalStrict(len(e.args)))
 		} else {

File diff suppressed because it is too large
+ 432 - 322
compiler_stmt.go


File diff suppressed because it is too large
+ 851 - 58
compiler_test.go


+ 17 - 8
parser/expression.go

@@ -141,7 +141,9 @@ func (self *_parser) parseRegExpLiteral() *ast.RegExpLiteral {
 }
 
 func (self *_parser) parseVariableDeclaration(declarationList *[]*ast.VariableExpression) ast.Expression {
-
+	if self.token == token.LET {
+		self.token = token.IDENTIFIER
+	}
 	if self.token != token.IDENTIFIER {
 		idx := self.expect(token.IDENTIFIER)
 		self.nextStatement()
@@ -168,25 +170,26 @@ func (self *_parser) parseVariableDeclaration(declarationList *[]*ast.VariableEx
 	return node
 }
 
-func (self *_parser) parseVariableDeclarationList(var_ file.Idx) []ast.Expression {
-
-	var declarationList []*ast.VariableExpression // Avoid bad expressions
-	var list []ast.Expression
-
+func (self *_parser) parseVariableDeclarationList() (declarationList []*ast.VariableExpression) {
 	for {
-		list = append(list, self.parseVariableDeclaration(&declarationList))
+		self.parseVariableDeclaration(&declarationList)
 		if self.token != token.COMMA {
 			break
 		}
 		self.next()
 	}
+	return
+}
+
+func (self *_parser) parseVarDeclarationList(var_ file.Idx) []*ast.VariableExpression {
+	declarationList := self.parseVariableDeclarationList()
 
 	self.scope.declare(&ast.VariableDeclaration{
 		Var:  var_,
 		List: declarationList,
 	})
 
-	return list
+	return declarationList
 }
 
 func (self *_parser) parseObjectPropertyKey() (unistring.String, ast.Expression, token.Token) {
@@ -778,6 +781,9 @@ func (self *_parser) parseConditionlExpression() ast.Expression {
 }
 
 func (self *_parser) parseAssignmentExpression() ast.Expression {
+	if self.token == token.LET {
+		self.token = token.IDENTIFIER
+	}
 	left := self.parseConditionlExpression()
 	var operator token.Token
 	switch self.token {
@@ -828,6 +834,9 @@ func (self *_parser) parseAssignmentExpression() ast.Expression {
 }
 
 func (self *_parser) parseExpression() ast.Expression {
+	if self.token == token.LET {
+		self.token = token.IDENTIFIER
+	}
 	next := self.parseAssignmentExpression
 	left := next()
 

+ 9 - 0
parser/lexer.go

@@ -149,6 +149,7 @@ func isId(tkn token.Token) bool {
 		token.DO,
 
 		token.VAR,
+		token.LET,
 		token.FOR,
 		token.NEW,
 		token.TRY,
@@ -158,6 +159,7 @@ func isId(tkn token.Token) bool {
 		token.VOID,
 		token.WITH,
 
+		token.CONST,
 		token.WHILE,
 		token.BREAK,
 		token.CATCH,
@@ -182,6 +184,13 @@ func isId(tkn token.Token) bool {
 	return false
 }
 
+func (self *_parser) peek() token.Token {
+	implicitSemicolon, insertSemicolon, chr, chrOffset, offset := self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset
+	tok, _, _, _ := self.scan()
+	self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset = implicitSemicolon, insertSemicolon, chr, chrOffset, offset
+	return tok
+}
+
 func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unistring.String, idx file.Idx) {
 
 	self.implicitSemicolon = false

+ 16 - 5
parser/marshal_test.go

@@ -117,7 +117,7 @@ func testMarshalNode(node interface{}) interface{} {
 
 	case *ast.ForInStatement:
 		return marshal("ForIn",
-			"Into", marshal("", node.Into),
+			"Into", testMarshalNode(node.Into),
 			"Source", marshal("", node.Source),
 			"Body", marshal("", node.Body),
 		)
@@ -161,6 +161,15 @@ func testMarshalNode(node interface{}) interface{} {
 	case *ast.VariableStatement:
 		return marshal("Var", testMarshalNode(node.List))
 
+	// Special
+	case *ast.ForDeclaration:
+		return marshal("For-Into-Decl", testMarshalNode(node.Binding))
+
+	case *ast.ForIntoVar:
+		return marshal("For-Into-Var", testMarshalNode(node.Binding))
+
+	case *ast.BindingIdentifier:
+		return marshal("Binding-Identifier", "Id", node.Name)
 	}
 
 	{
@@ -815,10 +824,12 @@ func TestParserAST(t *testing.T) {
       "Body": {
         "BlockStatement": []
       },
-      "Into": [
-        "abc",
-        null
-      ],
+      "Into": {
+		"For-Into-Var": [
+           "abc",
+           null
+        ]
+      },
       "Source": {
         "Identifier": "def"
       }

+ 4 - 4
parser/parser_test.go

@@ -271,8 +271,6 @@ func TestParserErr(t *testing.T) {
 
 		test("try {}", "(anonymous): Line 1:1 Missing catch or finally after try")
 
-		test("try {} catch {}", "(anonymous): Line 1:14 Unexpected token {")
-
 		test("try {} catch () {}", "(anonymous): Line 1:15 Unexpected token )")
 
 		test("\u203f = 1", "(anonymous): Line 1:1 Unexpected token ILLEGAL")
@@ -414,9 +412,9 @@ func TestParserErr(t *testing.T) {
 			test("abc.class = 1", nil)
 			test("var class;", "(anonymous): Line 1:5 Unexpected reserved word")
 
-			test("const", "(anonymous): Line 1:1 Unexpected reserved word")
+			test("const", "(anonymous): Line 1:6 Unexpected end of input")
 			test("abc.const = 1", nil)
-			test("var const;", "(anonymous): Line 1:5 Unexpected reserved word")
+			test("var const;", "(anonymous): Line 1:5 Unexpected token const")
 
 			test("enum", "(anonymous): Line 1:1 Unexpected reserved word")
 			test("abc.enum = 1", nil)
@@ -606,6 +604,8 @@ func TestParser(t *testing.T) {
 
 		test(`try {} catch (abc) {} finally {}`, nil)
 
+		test("try {} catch {}", nil)
+
 		test(`
             do {
                 do {

+ 3 - 2
parser/scope.go

@@ -8,10 +8,11 @@ import (
 type _scope struct {
 	outer           *_scope
 	allowIn         bool
+	allowLet        bool
 	inIteration     bool
 	inSwitch        bool
 	inFunction      bool
-	declarationList []ast.Declaration
+	declarationList []*ast.VariableDeclaration
 
 	labels []unistring.String
 }
@@ -27,7 +28,7 @@ func (self *_parser) closeScope() {
 	self.scope = self.scope.outer
 }
 
-func (self *_scope) declare(declaration ast.Declaration) {
+func (self *_scope) declare(declaration *ast.VariableDeclaration) {
 	self.declarationList = append(self.declarationList, declaration)
 }
 

+ 136 - 70
parser/statement.go

@@ -29,6 +29,7 @@ func (self *_parser) parseEmptyStatement() ast.Statement {
 
 func (self *_parser) parseStatementList() (list []ast.Statement) {
 	for self.token != token.RIGHT_BRACE && self.token != token.EOF {
+		self.scope.allowLet = true
 		list = append(list, self.parseStatement())
 	}
 
@@ -65,10 +66,18 @@ func (self *_parser) parseStatement() ast.Statement {
 		return self.parseWithStatement()
 	case token.VAR:
 		return self.parseVariableStatement()
+	case token.LET:
+		tok := self.peek()
+		if tok == token.LEFT_BRACKET || self.scope.allowLet && (tok == token.IDENTIFIER || tok == token.LET || tok == token.LEFT_BRACE) {
+			return self.parseLexicalDeclaration(self.token)
+		}
+		self.insertSemicolon = true
+	case token.CONST:
+		return self.parseLexicalDeclaration(self.token)
 	case token.FUNCTION:
-		self.parseFunction(true)
-		// FIXME
-		return &ast.EmptyStatement{}
+		return &ast.FunctionDeclaration{
+			Function: self.parseFunction(true),
+		}
 	case token.SWITCH:
 		return self.parseSwitchStatement()
 	case token.RETURN:
@@ -92,6 +101,7 @@ func (self *_parser) parseStatement() ast.Statement {
 			}
 		}
 		self.scope.labels = append(self.scope.labels, label) // Push the label
+		self.scope.allowLet = false
 		statement := self.parseStatement()
 		self.scope.labels = self.scope.labels[:len(self.scope.labels)-1] // Pop the label
 		return &ast.LabelledStatement{
@@ -118,20 +128,23 @@ func (self *_parser) parseTryStatement() ast.Statement {
 	if self.token == token.CATCH {
 		catch := self.idx
 		self.next()
-		self.expect(token.LEFT_PARENTHESIS)
-		if self.token != token.IDENTIFIER {
-			self.expect(token.IDENTIFIER)
-			self.nextStatement()
-			return &ast.BadStatement{From: catch, To: self.idx}
-		} else {
-			identifier := self.parseIdentifier()
-			self.expect(token.RIGHT_PARENTHESIS)
-			node.Catch = &ast.CatchStatement{
-				Catch:     catch,
-				Parameter: identifier,
-				Body:      self.parseBlockStatement(),
+		var parameter *ast.Identifier
+		if self.token == token.LEFT_PARENTHESIS {
+			self.next()
+			if self.token != token.IDENTIFIER {
+				self.expect(token.IDENTIFIER)
+				self.nextStatement()
+				return &ast.BadStatement{From: catch, To: self.idx}
+			} else {
+				parameter = self.parseIdentifier()
+				self.expect(token.RIGHT_PARENTHESIS)
 			}
 		}
+		node.Catch = &ast.CatchStatement{
+			Catch:     catch,
+			Parameter: parameter,
+			Body:      self.parseBlockStatement(),
+		}
 	}
 
 	if self.token == token.FINALLY {
@@ -192,11 +205,6 @@ func (self *_parser) parseFunction(declaration bool) *ast.FunctionLiteral {
 	var name *ast.Identifier
 	if self.token == token.IDENTIFIER {
 		name = self.parseIdentifier()
-		if declaration {
-			self.scope.declare(&ast.FunctionDeclaration{
-				Function: node,
-			})
-		}
 	} else if declaration {
 		// Use expect error handling
 		self.expect(token.IDENTIFIER)
@@ -322,7 +330,7 @@ func (self *_parser) parseWithStatement() ast.Statement {
 		Object: self.parseExpression(),
 	}
 	self.expect(token.RIGHT_PARENTHESIS)
-
+	self.scope.allowLet = false
 	node.Body = self.parseStatement()
 
 	return node
@@ -361,10 +369,11 @@ func (self *_parser) parseIterationStatement() ast.Statement {
 	defer func() {
 		self.scope.inIteration = inIteration
 	}()
+	self.scope.allowLet = false
 	return self.parseStatement()
 }
 
-func (self *_parser) parseForIn(idx file.Idx, into ast.Expression) *ast.ForInStatement {
+func (self *_parser) parseForIn(idx file.Idx, into ast.ForInto) *ast.ForInStatement {
 
 	// Already have consumed "<into> in"
 
@@ -379,11 +388,11 @@ func (self *_parser) parseForIn(idx file.Idx, into ast.Expression) *ast.ForInSta
 	}
 }
 
-func (self *_parser) parseForOf(idx file.Idx, into ast.Expression) *ast.ForOfStatement {
+func (self *_parser) parseForOf(idx file.Idx, into ast.ForInto) *ast.ForOfStatement {
 
 	// Already have consumed "<into> of"
 
-	source := self.parseExpression()
+	source := self.parseAssignmentExpression()
 	self.expect(token.RIGHT_PARENTHESIS)
 
 	return &ast.ForOfStatement{
@@ -394,7 +403,7 @@ func (self *_parser) parseForOf(idx file.Idx, into ast.Expression) *ast.ForOfSta
 	}
 }
 
-func (self *_parser) parseFor(idx file.Idx, initializer ast.Expression) *ast.ForStatement {
+func (self *_parser) parseFor(idx file.Idx, initializer ast.ForLoopInitializer) *ast.ForStatement {
 
 	// Already have consumed "<initializer> ;"
 
@@ -423,69 +432,120 @@ func (self *_parser) parseForOrForInStatement() ast.Statement {
 	idx := self.expect(token.FOR)
 	self.expect(token.LEFT_PARENTHESIS)
 
-	var left []ast.Expression
+	var initializer ast.ForLoopInitializer
 
 	forIn := false
 	forOf := false
+	var into ast.ForInto
 	if self.token != token.SEMICOLON {
 
 		allowIn := self.scope.allowIn
 		self.scope.allowIn = false
-		if self.token == token.VAR {
-			var_ := self.idx
+		tok := self.token
+		if tok == token.LET {
+			switch self.peek() {
+			case token.IDENTIFIER, token.LEFT_BRACKET, token.LEFT_BRACE:
+			default:
+				tok = token.IDENTIFIER
+			}
+		}
+		if tok == token.VAR || tok == token.LET || tok == token.CONST {
+			idx := self.idx
 			self.next()
-			list := self.parseVariableDeclarationList(var_)
+			var list []*ast.VariableExpression
+			if tok == token.VAR {
+				list = self.parseVarDeclarationList(idx)
+			} else {
+				list = self.parseVariableDeclarationList()
+			}
 			if len(list) == 1 {
 				if self.token == token.IN {
 					self.next() // in
 					forIn = true
-				} else if self.token == token.IDENTIFIER {
-					if self.literal == "of" {
-						self.next()
-						forOf = true
+				} else if self.token == token.IDENTIFIER && self.literal == "of" {
+					self.next()
+					forOf = true
+				}
+			}
+			if forIn || forOf {
+				if tok == token.VAR {
+					into = &ast.ForIntoVar{
+						Binding: list[0],
+					}
+				} else {
+					if list[0].Initializer != nil {
+						self.error(list[0].Initializer.Idx0(), "for-in loop variable declaration may not have an initializer")
+					}
+					into = &ast.ForDeclaration{
+						Idx:     idx,
+						IsConst: tok == token.CONST,
+						Binding: &ast.BindingIdentifier{
+							Name: list[0].Name,
+							Idx:  list[0].Idx,
+						},
+					}
+				}
+			} else {
+				if tok == token.VAR {
+					initializer = &ast.ForLoopInitializerVarDeclList{
+						List: list,
+					}
+				} else {
+					initializer = &ast.ForLoopInitializerLexicalDecl{
+						LexicalDeclaration: ast.LexicalDeclaration{
+							Idx:   idx,
+							Token: tok,
+							List:  list,
+						},
 					}
 				}
 			}
-			left = list
 		} else {
-			left = append(left, self.parseExpression())
+			expr := self.parseExpression()
 			if self.token == token.IN {
 				self.next()
 				forIn = true
-			} else if self.token == token.IDENTIFIER {
-				if self.literal == "of" {
-					self.next()
-					forOf = true
+			} else if self.token == token.IDENTIFIER && self.literal == "of" {
+				self.next()
+				forOf = true
+			}
+			if forIn || forOf {
+				switch expr.(type) {
+				case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression, *ast.VariableExpression:
+					// These are all acceptable
+				default:
+					self.error(idx, "Invalid left-hand side in for-in or for-of")
+					self.nextStatement()
+					return &ast.BadStatement{From: idx, To: self.idx}
+				}
+				into = &ast.ForIntoExpression{
+					Expression: expr,
+				}
+			} else {
+				initializer = &ast.ForLoopInitializerExpression{
+					Expression: expr,
 				}
 			}
 		}
 		self.scope.allowIn = allowIn
 	}
 
-	if forIn || forOf {
-		switch left[0].(type) {
-		case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression, *ast.VariableExpression:
-			// These are all acceptable
-		default:
-			self.error(idx, "Invalid left-hand side in for-in or for-of")
-			self.nextStatement()
-			return &ast.BadStatement{From: idx, To: self.idx}
-		}
-		if forIn {
-			return self.parseForIn(idx, left[0])
-		}
-		return self.parseForOf(idx, left[0])
+	if forIn {
+		return self.parseForIn(idx, into)
+	}
+	if forOf {
+		return self.parseForOf(idx, into)
 	}
 
 	self.expect(token.SEMICOLON)
-	return self.parseFor(idx, &ast.SequenceExpression{Sequence: left})
+	return self.parseFor(idx, initializer)
 }
 
 func (self *_parser) parseVariableStatement() *ast.VariableStatement {
 
 	idx := self.expect(token.VAR)
 
-	list := self.parseVariableDeclarationList(idx)
+	list := self.parseVarDeclarationList(idx)
 	self.semicolon()
 
 	return &ast.VariableStatement{
@@ -494,6 +554,22 @@ func (self *_parser) parseVariableStatement() *ast.VariableStatement {
 	}
 }
 
+func (self *_parser) parseLexicalDeclaration(tok token.Token) *ast.LexicalDeclaration {
+	idx := self.expect(tok)
+	if !self.scope.allowLet {
+		self.error(idx, "Lexical declaration cannot appear in a single-statement context")
+	}
+
+	list := self.parseVariableDeclarationList()
+	self.semicolon()
+
+	return &ast.LexicalDeclaration{
+		Idx:   idx,
+		Token: tok,
+		List:  list,
+	}
+}
+
 func (self *_parser) parseDoWhileStatement() ast.Statement {
 	inIteration := self.scope.inIteration
 	self.scope.inIteration = true
@@ -506,6 +582,7 @@ func (self *_parser) parseDoWhileStatement() ast.Statement {
 	if self.token == token.LEFT_BRACE {
 		node.Body = self.parseBlockStatement()
 	} else {
+		self.scope.allowLet = false
 		node.Body = self.parseStatement()
 	}
 
@@ -543,34 +620,23 @@ func (self *_parser) parseIfStatement() ast.Statement {
 	if self.token == token.LEFT_BRACE {
 		node.Consequent = self.parseBlockStatement()
 	} else {
+		self.scope.allowLet = false
 		node.Consequent = self.parseStatement()
 	}
 
 	if self.token == token.ELSE {
 		self.next()
+		self.scope.allowLet = false
 		node.Alternate = self.parseStatement()
 	}
 
 	return node
 }
 
-func (self *_parser) parseSourceElement() ast.Statement {
-	return self.parseStatement()
-}
-
-func (self *_parser) parseSourceElements() []ast.Statement {
-	body := []ast.Statement(nil)
-
-	for {
-		if self.token != token.STRING {
-			break
-		}
-
-		body = append(body, self.parseSourceElement())
-	}
-
+func (self *_parser) parseSourceElements() (body []ast.Statement) {
 	for self.token != token.EOF {
-		body = append(body, self.parseSourceElement())
+		self.scope.allowLet = true
+		body = append(body, self.parseStatement())
 	}
 
 	return body

+ 106 - 65
runtime.go

@@ -44,6 +44,9 @@ const (
 )
 
 type global struct {
+	stash    stash
+	varNames map[unistring.String]struct{}
+
 	Object   *Object
 	Array    *Object
 	Function *Object
@@ -759,31 +762,27 @@ func (r *Runtime) builtin_thrower(FunctionCall) Value {
 
 func (r *Runtime) eval(srcVal valueString, direct, strict bool, this Value) Value {
 	src := escapeInvalidUtf16(srcVal)
-	p, err := r.compile("<eval>", src, strict, true)
+	vm := r.vm
+	p, err := r.compile("<eval>", src, strict, true, !direct || vm.stash == &r.global.stash)
 	if err != nil {
 		panic(err)
 	}
 
-	vm := r.vm
-
 	vm.pushCtx()
 	vm.prg = p
 	vm.pc = 0
+	vm.args = 0
+	vm.result = _undefined
 	if !direct {
-		vm.stash = nil
+		vm.stash = &r.global.stash
 	}
 	vm.sb = vm.sp
 	vm.push(this)
-	if strict {
-		vm.push(valueTrue)
-	} else {
-		vm.push(valueFalse)
-	}
 	vm.run()
+	retval := vm.result
 	vm.popCtx()
 	vm.halt = false
-	retval := vm.stack[vm.sp-1]
-	vm.sp -= 2
+	vm.sp -= 1
 	return retval
 }
 
@@ -1083,14 +1082,14 @@ func New() *Runtime {
 // method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly
 // at the same time).
 func Compile(name, src string, strict bool) (*Program, error) {
-	return compile(name, src, strict, false)
+	return compile(name, src, strict, false, true)
 }
 
 // CompileAST creates an internal representation of the JavaScript code that can be later run using the Runtime.RunProgram()
 // method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly
 // at the same time).
 func CompileAST(prg *js_ast.Program, strict bool) (*Program, error) {
-	return compileAST(prg, strict, false)
+	return compileAST(prg, strict, false, true)
 }
 
 // MustCompile is like Compile but panics if the code cannot be compiled.
@@ -1126,19 +1125,17 @@ func Parse(name, src string, options ...parser.Option) (prg *js_ast.Program, err
 	return
 }
 
-func compile(name, src string, strict, eval bool, parserOptions ...parser.Option) (p *Program, err error) {
+func compile(name, src string, strict, eval, inGlobal bool, parserOptions ...parser.Option) (p *Program, err error) {
 	prg, err := Parse(name, src, parserOptions...)
 	if err != nil {
 		return
 	}
 
-	return compileAST(prg, strict, eval)
+	return compileAST(prg, strict, eval, inGlobal)
 }
 
-func compileAST(prg *js_ast.Program, strict, eval bool) (p *Program, err error) {
+func compileAST(prg *js_ast.Program, strict, eval, inGlobal bool) (p *Program, err error) {
 	c := newCompiler()
-	c.scope.strict = strict
-	c.scope.eval = eval
 
 	defer func() {
 		if x := recover(); x != nil {
@@ -1152,13 +1149,13 @@ func compileAST(prg *js_ast.Program, strict, eval bool) (p *Program, err error)
 		}
 	}()
 
-	c.compile(prg)
+	c.compile(prg, strict, eval, inGlobal)
 	p = c.p
 	return
 }
 
-func (r *Runtime) compile(name, src string, strict, eval bool) (p *Program, err error) {
-	p, err = compile(name, src, strict, eval, r.parserOptions...)
+func (r *Runtime) compile(name, src string, strict, eval, inGlobal bool) (p *Program, err error) {
+	p, err = compile(name, src, strict, eval, inGlobal, r.parserOptions...)
 	if err != nil {
 		switch x1 := err.(type) {
 		case *CompilerSyntaxError:
@@ -1181,7 +1178,7 @@ func (r *Runtime) RunString(str string) (Value, error) {
 
 // RunScript executes the given string in the global context.
 func (r *Runtime) RunScript(name, src string) (Value, error) {
-	p, err := r.compile(name, src, false, false)
+	p, err := r.compile(name, src, false, false, true)
 
 	if err != nil {
 		return nil, err
@@ -1201,25 +1198,30 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
 			}
 		}
 	}()
+	vm := r.vm
 	recursive := false
-	if len(r.vm.callStack) > 0 {
+	if len(vm.callStack) > 0 {
 		recursive = true
-		r.vm.pushCtx()
+		vm.pushCtx()
+		vm.stash = &r.global.stash
+		vm.sb = vm.sp - 1
 	}
-	r.vm.prg = p
-	r.vm.pc = 0
-	ex := r.vm.runTry()
+	vm.prg = p
+	vm.pc = 0
+	vm.result = _undefined
+	ex := vm.runTry()
 	if ex == nil {
-		result = r.vm.pop()
+		result = r.vm.result
 	} else {
 		err = ex
 	}
 	if recursive {
-		r.vm.popCtx()
-		r.vm.halt = false
-		r.vm.clearStack()
+		vm.popCtx()
+		vm.halt = false
+		vm.clearStack()
 	} else {
-		r.vm.stack = nil
+		vm.stack = nil
+		vm.prg = nil
 		r.leave()
 	}
 	return
@@ -1959,8 +1961,7 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.
 // ExportTo converts a JavaScript value into the specified Go value. The second parameter must be a non-nil pointer.
 // Exporting to an interface{} results in a value of the same type as Export() would produce.
 // Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning
-// values to non-clamped typed array items, e.g.
-// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-toint32
+// values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32
 // Returns error if conversion is not possible.
 func (r *Runtime) ExportTo(v Value, target interface{}) error {
 	tval := reflect.ValueOf(target)
@@ -1975,15 +1976,38 @@ func (r *Runtime) GlobalObject() *Object {
 	return r.globalObject
 }
 
-// Set the specified value as a property of the global object.
-// The value is first converted using ToValue()
-func (r *Runtime) Set(name string, value interface{}) {
-	r.globalObject.self.setOwnStr(unistring.NewFromString(name), r.ToValue(value), false)
+// Set the specified variable in the global context.
+// Equivalent to running "name = value" in non-strict mode.
+// The value is first converted using ToValue().
+// Note, this is not the same as GlobalObject().Set(name, value),
+// because if a global lexical binding (let or const) exists, it is set instead.
+func (r *Runtime) Set(name string, value interface{}) error {
+	return r.try(func() {
+		name := unistring.NewFromString(name)
+		v := r.ToValue(value)
+		if ref := r.global.stash.getRefByName(name, false); ref != nil {
+			ref.set(v)
+		} else {
+			r.globalObject.self.setOwnStr(name, v, true)
+		}
+	})
 }
 
-// Get the specified property of the global object.
-func (r *Runtime) Get(name string) Value {
-	return r.globalObject.self.getStr(unistring.NewFromString(name), nil)
+// Get the specified variable in the global context.
+// Equivalent to dereferencing a variable by name in non-strict mode. If variable is not defined returns nil.
+// Note, this is not the same as GlobalObject().Get(name),
+// because if a global lexical binding (let or const) exists, it is used instead.
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
+func (r *Runtime) Get(name string) (ret Value) {
+	r.tryPanic(func() {
+		n := unistring.NewFromString(name)
+		if v, exists := r.global.stash.getByName(n); exists {
+			ret = v
+		} else {
+			ret = r.globalObject.self.getStr(n, nil)
+		}
+	})
+	return
 }
 
 // SetRandSource sets random source for this Runtime. If not called, the default math/rand is used.
@@ -2004,7 +2028,7 @@ func (r *Runtime) SetParserOptions(opts ...parser.Option) {
 
 // New is an equivalent of the 'new' operator allowing to call it directly from Go.
 func (r *Runtime) New(construct Value, args ...Value) (o *Object, err error) {
-	err = tryFunc(func() {
+	err = r.try(func() {
 		o = r.builtin_new(r.toObject(construct), args)
 	})
 	return
@@ -2095,29 +2119,28 @@ func NegativeInf() Value {
 	return _negativeInf
 }
 
-func tryFunc(f func()) (err error) {
+func tryFunc(f func()) (ret interface{}) {
 	defer func() {
-		if x := recover(); x != nil {
-			switch x := x.(type) {
-			case *Exception:
-				err = x
-			case *InterruptedError:
-				err = x
-			case Value:
-				err = &Exception{
-					val: x,
-				}
-			default:
-				panic(x)
-			}
-		}
+		ret = recover()
 	}()
 
 	f()
+	return
+}
 
+func (r *Runtime) try(f func()) error {
+	if ex := r.vm.try(f); ex != nil {
+		return ex
+	}
 	return nil
 }
 
+func (r *Runtime) tryPanic(f func()) {
+	if ex := r.vm.try(f); ex != nil {
+		panic(ex)
+	}
+}
+
 func (r *Runtime) toObject(v Value, args ...interface{}) *Object {
 	if obj, ok := v.(*Object); ok {
 		return obj
@@ -2212,9 +2235,7 @@ func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Objec
 func returnIter(iter *Object) {
 	retMethod := toMethod(iter.self.getStr("return", nil))
 	if retMethod != nil {
-		_ = tryFunc(func() {
-			retMethod(FunctionCall{This: iter})
-		})
+		iter.runtime.toObject(retMethod(FunctionCall{This: iter}))
 	}
 }
 
@@ -2224,12 +2245,15 @@ func (r *Runtime) iterate(iter *Object, step func(Value)) {
 		if nilSafe(res.self.getStr("done", nil)).ToBoolean() {
 			break
 		}
-		err := tryFunc(func() {
-			step(nilSafe(res.self.getStr("value", nil)))
+		value := nilSafe(res.self.getStr("value", nil))
+		ret := tryFunc(func() {
+			step(value)
 		})
-		if err != nil {
-			returnIter(iter)
-			panic(err)
+		if ret != nil {
+			_ = tryFunc(func() {
+				returnIter(iter)
+			})
+			panic(ret)
 		}
 	}
 }
@@ -2353,6 +2377,23 @@ func (r *Runtime) genId() (ret uint64) {
 	return
 }
 
+func (r *Runtime) setGlobal(name unistring.String, v Value, strict bool) {
+	if ref := r.global.stash.getRefByName(name, strict); ref != nil {
+		ref.set(v)
+	} else {
+		o := r.globalObject.self
+		if strict {
+			if o.hasOwnPropertyStr(name) {
+				o.setOwnStr(name, v, true)
+			} else {
+				r.throwReferenceError(name)
+			}
+		} else {
+			o.setOwnStr(name, v, false)
+		}
+	}
+}
+
 func strPropToInt(s unistring.String) (int, bool) {
 	if res, err := strconv.Atoi(string(s)); err == nil {
 		return res, true

+ 71 - 1
runtime_test.go

@@ -168,9 +168,12 @@ func TestSetFunc(t *testing.T) {
 	sum(40, 2);
 	`
 	r := New()
-	r.Set("sum", func(call FunctionCall) Value {
+	err := r.Set("sum", func(call FunctionCall) Value {
 		return r.ToValue(call.Argument(0).ToInteger() + call.Argument(1).ToInteger())
 	})
+	if err != nil {
+		t.Fatal(err)
+	}
 	v, err := r.RunString(SCRIPT)
 	if err != nil {
 		t.Fatal(err)
@@ -180,6 +183,53 @@ func TestSetFunc(t *testing.T) {
 	}
 }
 
+func ExampleRuntime_Set_lexical() {
+	r := New()
+	_, err := r.RunString("let x")
+	if err != nil {
+		panic(err)
+	}
+	err = r.Set("x", 1)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Print(r.Get("x"), r.GlobalObject().Get("x"))
+	// Output: 1 <nil>
+}
+
+func TestRecursiveRun(t *testing.T) {
+	// Make sure that a recursive call to Run*() correctly sets the environment and no stash or stack
+	// corruptions occur.
+	vm := New()
+	vm.Set("f", func() (Value, error) {
+		return vm.RunString("let x = 1; { let z = 100, z1 = 200, z2 = 300, z3 = 400; } x;")
+	})
+	res, err := vm.RunString(`
+	function f1() {
+		let x = 2;
+		eval('');
+		{
+			let y = 3;
+			let res = f();
+			if (x !== 2) { // check for stash corruption
+				throw new Error("x="+x);
+			}
+			if (y !== 3) { // check for stack corruption
+				throw new Error("y="+y);
+			}
+			return res;
+		}
+	};
+	f1();
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !res.SameAs(valueInt(1)) {
+		t.Fatal(res)
+	}
+}
+
 func TestObjectGetSet(t *testing.T) {
 	const SCRIPT = `
 		input.test++;
@@ -1901,6 +1951,26 @@ func TestAbandonedEnumerate(t *testing.T) {
 	testScript1(SCRIPT, asciiString("baz-foo foo-foo bar-foo "), t)
 }
 
+func TestDeclareGlobalFunc(t *testing.T) {
+	const SCRIPT = `
+	var initial;
+
+	Object.defineProperty(this, 'f', {
+	  enumerable: true,
+	  writable: true,
+	  configurable: false
+	});
+
+	(0,eval)('initial = f; function f() { return 2222; }');
+	var desc = Object.getOwnPropertyDescriptor(this, "f");
+	assert(desc.enumerable, "enumerable");
+	assert(desc.writable, "writable");
+	assert(!desc.configurable, "configurable");
+	assert.sameValue(initial(), 2222);
+	`
+	testScript1(TESTLIB+SCRIPT, _undefined, t)
+}
+
 /*
 func TestArrayConcatSparse(t *testing.T) {
 function foo(a,b,c)

+ 69 - 46
tc39_test.go

@@ -114,6 +114,7 @@ var (
 		"test/language/expressions/object/method.js":                                                                 true,
 		"test/language/expressions/object/setter-super-prop.js":                                                      true,
 		"test/language/expressions/object/getter-super-prop.js":                                                      true,
+		"test/language/expressions/delete/super-property.js":                                                         true,
 
 		// object literals
 		"test/built-ins/Array/from/source-object-iterator-1.js":                                                true,
@@ -178,6 +179,16 @@ var (
 		"test/built-ins/Number/prototype/toExponential/range.js":                true,
 		"test/built-ins/Number/prototype/toFixed/range.js":                      true,
 		"test/built-ins/Number/prototype/toPrecision/range.js":                  true,
+		"test/built-ins/TypedArray/prototype/sort/stability.js":                 true,
+		"test/built-ins/RegExp/named-groups/functional-replace-global.js":       true,
+		"test/built-ins/RegExp/named-groups/functional-replace-non-global.js":   true,
+		"test/built-ins/Array/prototype/sort/stability-513-elements.js":         true,
+		"test/built-ins/Array/prototype/sort/stability-5-elements.js":           true,
+		"test/built-ins/Array/prototype/sort/stability-2048-elements.js":        true,
+		"test/built-ins/Array/prototype/sort/stability-11-elements.js":          true,
+		"test/language/statements/variable/fn-name-arrow.js":                    true,
+		"test/language/statements/let/fn-name-arrow.js":                         true,
+		"test/language/statements/const/fn-name-arrow.js":                       true,
 
 		// template strings
 		"test/built-ins/String/raw/zero-literal-segments.js":                                           true,
@@ -238,28 +249,6 @@ var (
 		"test/built-ins/Array/prototype/unshift/throws-if-integer-limit-exceeded.js":              true,
 		"test/built-ins/String/prototype/split/separator-undef-limit-custom.js":                   true,
 
-		// block-scoped vars
-		"test/built-ins/Array/prototype/sort/stability-11-elements.js":                true,
-		"test/built-ins/Array/prototype/sort/stability-2048-elements.js":              true,
-		"test/built-ins/Array/prototype/sort/stability-5-elements.js":                 true,
-		"test/built-ins/Array/prototype/sort/stability-513-elements.js":               true,
-		"test/built-ins/Date/parse/time-value-maximum-range.js":                       true,
-		"test/built-ins/Date/parse/without-utc-offset.js":                             true,
-		"test/built-ins/Date/parse/zero.js":                                           true,
-		"test/built-ins/Date/prototype/toDateString/format.js":                        true,
-		"test/built-ins/Date/prototype/toString/format.js":                            true,
-		"test/built-ins/Date/prototype/toTimeString/format.js":                        true,
-		"test/built-ins/Date/prototype/toUTCString/format.js":                         true,
-		"test/built-ins/Number/prototype/toString/a-z.js":                             true,
-		"test/built-ins/RegExp/named-groups/functional-replace-global.js":             true,
-		"test/built-ins/RegExp/named-groups/functional-replace-non-global.js":         true,
-		"test/built-ins/RegExp/prototype/Symbol.replace/poisoned-stdlib.js":           true,
-		"test/built-ins/TypedArray/prototype/sort/stability.js":                       true,
-		"test/annexB/built-ins/RegExp/prototype/compile/this-cross-realm-instance.js": true,
-		"test/annexB/built-ins/RegExp/prototype/compile/this-subclass-instance.js":    true,
-		"test/built-ins/Array/prototype/flatMap/proxy-access-count.js":                true,
-		"test/built-ins/Array/prototype/flat/proxy-access-count.js":                   true,
-
 		// generators
 		"test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js": true,
 
@@ -272,11 +261,21 @@ var (
 
 		// get [Symbol.*]
 		"test/language/expressions/object/prop-def-id-eval-error.js": true,
+
+		// destructing binding
+		"test/language/statements/for-of/head-var-bound-names-dup.js": true,
+		"test/language/statements/for-of/head-let-destructuring.js":   true,
+		"test/language/statements/for-in/head-var-bound-names-dup.js": true,
+		"test/language/statements/for/head-let-destructuring.js":      true,
+		"test/language/statements/for-in/head-let-destructuring.js":   true,
 	}
 
 	featuresBlackList = []string{
 		"arrow-function",
+		"async-iteration",
 		"BigInt",
+		"class",
+		"destructuring-binding",
 		"generators",
 		"String.prototype.replaceAll",
 		"computed-property-names",
@@ -289,41 +288,55 @@ var (
 	es6IdWhiteList = []string{
 		"8.1.2.1",
 		"9.5",
+		"12.1",
+		"12.2.1",
+		"12.2.2",
 		"12.2.5",
 		"12.2.6.1",
 		"12.2.6.8",
-		"12.9.3",
-		"12.9.4",
+		"12.4",
+		"12.5",
+		"12.6",
+		"12.7",
+		"12.8",
+		"12.9",
+		"12.10",
+		"13.1",
+		"13.2",
+		"13.3",
+		"13.4",
+		"13.5",
+		"13.6",
+		"13.7",
+		"13.8",
+		"13.9",
+		"13.10",
+		"13.11",
+		"13.12",
+		"13.13",
+		"13.14",
+		"13.15",
 		"14.3.8",
-		"19.1",
-		"19.2",
-		"19.3",
-		"19.4",
-		"19.5",
-		"20.1",
-		"20.2",
-		"20.3",
-		"21.1",
-		"21.2",
-		"22.1",
-		"22.2",
-		"23.1",
-		"23.2",
-		"23.3",
-		"23.4",
-		"24.1",
-		"24.2",
-		"24.3",
-		"25.1.2",
-		"26.1",
-		"26.2",
+		"18",
+		"19",
+		"20",
+		"21",
+		"22",
+		"23",
+		"24",
+		"25.1",
+		"26",
 		"B.2.1",
 		"B.2.2",
 	}
 
 	esIdPrefixWhiteList = []string{
+		"sec-addition-*",
 		"sec-array",
 		"sec-%typedarray%",
+		"sec-%typedarray%-of",
+		"sec-@@iterator",
+		"sec-@@tostringtag",
 		"sec-string",
 		"sec-date",
 		"sec-json",
@@ -340,6 +353,16 @@ var (
 		"sec-object.values",
 		"sec-object-initializer",
 		"sec-proxy-*",
+		"sec-for-statement-*",
+		"sec-for-in-and-for-of-statements",
+		"sec-do-while-statement",
+		"sec-if-statement",
+		"sec-while-statement",
+		"sec-with-statement*",
+		"sec-switch-*",
+		"sec-try-*",
+		"sec-strict-mode-of-ecmascript",
+		"sec-let-and-const-declarations*",
 	}
 )
 

+ 7 - 5
token/token_const.go

@@ -79,6 +79,7 @@ const (
 	DO
 
 	VAR
+	LET
 	FOR
 	NEW
 	TRY
@@ -89,6 +90,7 @@ const (
 	VOID
 	WITH
 
+	CONST
 	WHILE
 	BREAK
 	CATCH
@@ -173,6 +175,7 @@ var token2string = [...]string{
 	OF:                          "of",
 	DO:                          "do",
 	VAR:                         "var",
+	LET:                         "let",
 	FOR:                         "for",
 	NEW:                         "new",
 	TRY:                         "try",
@@ -181,6 +184,7 @@ var token2string = [...]string{
 	CASE:                        "case",
 	VOID:                        "void",
 	WITH:                        "with",
+	CONST:                       "const",
 	WHILE:                       "while",
 	BREAK:                       "break",
 	CATCH:                       "catch",
@@ -277,8 +281,7 @@ var keywordTable = map[string]_keyword{
 		token: INSTANCEOF,
 	},
 	"const": {
-		token:         KEYWORD,
-		futureKeyword: true,
+		token: CONST,
 	},
 	"class": {
 		token:         KEYWORD,
@@ -315,9 +318,8 @@ var keywordTable = map[string]_keyword{
 		strict:        true,
 	},
 	"let": {
-		token:         KEYWORD,
-		futureKeyword: true,
-		strict:        true,
+		token:  LET,
+		strict: true,
 	},
 	"package": {
 		token:         KEYWORD,

+ 38 - 14
value.go

@@ -80,6 +80,7 @@ type valueContainer interface {
 
 type typeError string
 type rangeError string
+type referenceError string
 
 type valueInt int64
 type valueFloat float64
@@ -117,6 +118,11 @@ type valueProperty struct {
 	setterFunc   *Object
 }
 
+var (
+	errAccessBeforeInit = referenceError("Cannot access a variable before initialization")
+	errAssignToConst    = typeError("Assignment to constant variable.")
+)
+
 func propGetter(o Value, v Value, r *Runtime) *Object {
 	if v == _undefined {
 		return nil
@@ -199,7 +205,7 @@ func (i valueInt) Equals(other Value) bool {
 	case valueBool:
 		return int64(i) == o.ToInteger()
 	case *Object:
-		return i.Equals(o.toPrimitiveNumber())
+		return i.Equals(o.toPrimitive())
 	}
 
 	return false
@@ -620,7 +626,7 @@ func (f valueFloat) Equals(other Value) bool {
 	case valueString, valueBool:
 		return float64(f) == o.ToFloat()
 	case *Object:
-		return f.Equals(o.toPrimitiveNumber())
+		return f.Equals(o.toPrimitive())
 	}
 
 	return false
@@ -705,7 +711,7 @@ func (o *Object) Equals(other Value) bool {
 	}
 
 	switch o1 := other.(type) {
-	case valueInt, valueFloat, valueString:
+	case valueInt, valueFloat, valueString, *Symbol:
 		return o.toPrimitive().Equals(other)
 	case valueBool:
 		return o.Equals(o1.ToNumber())
@@ -725,8 +731,15 @@ func (o *Object) baseObject(*Runtime) *Object {
 	return o
 }
 
-func (o *Object) Export() interface{} {
-	return o.self.export(&objectExportCtx{})
+// Export the Object to a plain Go type. The returned value will be map[string]interface{} unless
+// the Object is a wrapped Go value (created using ToValue()).
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
+func (o *Object) Export() (ret interface{}) {
+	o.runtime.tryPanic(func() {
+		ret = o.self.export(&objectExportCtx{})
+	})
+
+	return
 }
 
 func (o *Object) ExportType() reflect.Type {
@@ -737,16 +750,21 @@ func (o *Object) hash(*maphash.Hash) uint64 {
 	return o.getId()
 }
 
+// Get an object's property by name.
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
 func (o *Object) Get(name string) Value {
 	return o.self.getStr(unistring.NewFromString(name), nil)
 }
 
 // GetSymbol returns the value of a symbol property. Use one of the Sym* values for well-known
 // symbols (such as SymIterator, SymToStringTag, etc...).
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
 func (o *Object) GetSymbol(sym *Symbol) Value {
 	return o.self.getSym(sym, nil)
 }
 
+// Keys returns a list of Object's enumerable keys.
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
 func (o *Object) Keys() (keys []string) {
 	iter := &enumerableIter{
 		wrapped: o.self.enumerateOwnKeys(),
@@ -758,6 +776,8 @@ func (o *Object) Keys() (keys []string) {
 	return
 }
 
+// Symbols returns a list of Object's enumerable symbol properties.
+// This method will panic with an *Exception if a JavaScript exception is thrown in the process.
 func (o *Object) Symbols() []*Symbol {
 	symbols := o.self.ownSymbols(false, nil)
 	ret := make([]*Symbol, len(symbols))
@@ -770,7 +790,7 @@ func (o *Object) Symbols() []*Symbol {
 // DefineDataProperty is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable,
 // configurable: configurable, enumerable: enumerable})
 func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{
 			Value:        value,
 			Writable:     writable,
@@ -783,7 +803,7 @@ func (o *Object) DefineDataProperty(name string, value Value, writable, configur
 // DefineAccessorProperty is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter,
 // configurable: configurable, enumerable: enumerable})
 func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{
 			Getter:       getter,
 			Setter:       setter,
@@ -796,7 +816,7 @@ func (o *Object) DefineAccessorProperty(name string, getter, setter Value, confi
 // DefineDataPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable,
 // configurable: configurable, enumerable: enumerable})
 func (o *Object) DefineDataPropertySymbol(name *Symbol, value Value, writable, configurable, enumerable Flag) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.defineOwnPropertySym(name, PropertyDescriptor{
 			Value:        value,
 			Writable:     writable,
@@ -809,7 +829,7 @@ func (o *Object) DefineDataPropertySymbol(name *Symbol, value Value, writable, c
 // DefineAccessorPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter,
 // configurable: configurable, enumerable: enumerable})
 func (o *Object) DefineAccessorPropertySymbol(name *Symbol, getter, setter Value, configurable, enumerable Flag) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.defineOwnPropertySym(name, PropertyDescriptor{
 			Getter:       getter,
 			Setter:       setter,
@@ -820,25 +840,25 @@ func (o *Object) DefineAccessorPropertySymbol(name *Symbol, getter, setter Value
 }
 
 func (o *Object) Set(name string, value interface{}) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.setOwnStr(unistring.NewFromString(name), o.runtime.ToValue(value), true)
 	})
 }
 
 func (o *Object) SetSymbol(name *Symbol, value interface{}) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.setOwnSym(name, o.runtime.ToValue(value), true)
 	})
 }
 
 func (o *Object) Delete(name string) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.deleteStr(unistring.NewFromString(name), true)
 	})
 }
 
 func (o *Object) DeleteSymbol(name *Symbol) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.deleteSym(name, true)
 	})
 }
@@ -852,7 +872,7 @@ func (o *Object) Prototype() *Object {
 // SetPrototype sets the Object's prototype, same as Object.setPrototypeOf(). Setting proto to nil
 // is an equivalent of Object.setPrototypeOf(null).
 func (o *Object) SetPrototype(proto *Object) error {
-	return tryFunc(func() {
+	return o.runtime.try(func() {
 		o.self.setProto(proto, true)
 	})
 }
@@ -1007,6 +1027,10 @@ func (s *Symbol) SameAs(other Value) bool {
 }
 
 func (s *Symbol) Equals(o Value) bool {
+	switch o := o.(type) {
+	case *Object:
+		return s.Equals(o.toPrimitive())
+	}
 	return s.SameAs(o)
 }
 

File diff suppressed because it is too large
+ 511 - 188
vm.go


+ 4 - 3
vm_test.go

@@ -2,6 +2,7 @@ package goja
 
 import (
 	"github.com/dop251/goja/parser"
+	"github.com/dop251/goja/unistring"
 	"testing"
 )
 
@@ -14,7 +15,7 @@ func TestVM1(t *testing.T) {
 	vm.prg = &Program{
 		values: []Value{valueInt(2), valueInt(3), asciiString("test")},
 		code: []instruction{
-			bindName("v"),
+			&bindGlobal{vars: []unistring.String{"v"}},
 			newObject,
 			setGlobal("v"),
 			loadVal(2),
@@ -23,7 +24,7 @@ func TestVM1(t *testing.T) {
 			add,
 			setElem,
 			pop,
-			getVar1("v"),
+			loadDynamic("v"),
 			halt,
 		},
 	}
@@ -264,7 +265,7 @@ fib(35);
 	}
 
 	c := newCompiler()
-	c.compile(prg)
+	c.compile(prg, false, false, true)
 	c.p.dumpCode(b.Logf)
 
 	r := &Runtime{}

Some files were not shown because too many files changed in this diff