diff --git a/pkg/builtins/object_init.go b/pkg/builtins/object_init.go index 49b892c4..d6667f25 100644 --- a/pkg/builtins/object_init.go +++ b/pkg/builtins/object_init.go @@ -169,15 +169,8 @@ func (o *ObjectInitializer) InitRuntime(ctx *RuntimeContext) error { switch thisValue.Type() { case vm.TypeObject: plainObj := thisValue.AsPlainObject() - // Special case for globalThis: check heap for top-level declarations - if plainObj == vmInstance.GlobalObject { - if idx, exists := vmInstance.GetHeap().GetNameToIndex()[propName]; exists { - // Also verify the heap value is still initialized (not deleted) - if _, isInit := vmInstance.GetHeap().Get(idx); isInit { - return vm.BooleanValue(true), nil - } - } - } + // globalThis's var/function bindings are mirrored as own + // properties; its let/const/class bindings are not properties. _, hasOwn := plainObj.GetOwn(propName) return vm.BooleanValue(hasOwn), nil case vm.TypeDictObject: diff --git a/pkg/builtins/reflect_delete.go b/pkg/builtins/reflect_delete.go index 39a31b24..d65a2063 100644 --- a/pkg/builtins/reflect_delete.go +++ b/pkg/builtins/reflect_delete.go @@ -58,8 +58,8 @@ func reflectDeleteProperty(vmInstance *vm.VM, target vm.Value, key vm.Value) (bo } success := deleteFromTable(po, name) if success && po == vmInstance.GlobalObject && heap != nil { - if idx, exists := heap.GetNameToIndex()[name]; exists { - heap.Delete(idx) + if idx, exists := heap.GetNameToIndex()[name]; exists && heap.Delete(idx) { + vmInstance.UnmirrorGlobalVar(idx) } } return success, nil diff --git a/pkg/compiler/compile_class.go b/pkg/compiler/compile_class.go index 0bfefe39..66092b6d 100644 --- a/pkg/compiler/compile_class.go +++ b/pkg/compiler/compile_class.go @@ -505,7 +505,7 @@ func (c *Compiler) compileClassDeclaration(node *parser.ClassDeclaration, hint R // decorator replaced it (#608). if node.Name.Value != "" { if isGlobalClassScope { - c.emitSetGlobal(classGlobalIdx, constructorReg, node.Token.Line) + c.emitSetGlobalInit(classGlobalIdx, constructorReg, node.Token.Line) } else { c.emitStoreSpill(classSpillSlot, constructorReg, node.Token.Line) } @@ -539,7 +539,7 @@ func (c *Compiler) compileClassDeclaration(node *parser.ClassDeclaration, hint R // spill slot (defined up front in step 1), so a single store satisfies both. if isGlobalClassScope { // Top-level class declaration - set the global value - c.emitSetGlobal(classGlobalIdx, constructorReg, node.Token.Line) + c.emitSetGlobalInit(classGlobalIdx, constructorReg, node.Token.Line) debugPrintf("// DEBUG compileClassDeclaration: Set global class '%s' to R%d\n", node.Name.Value, constructorReg) } else { c.emitStoreSpill(classSpillSlot, constructorReg, node.Token.Line) diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index e7a8b358..ced7057e 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -1382,6 +1382,27 @@ func (c *Compiler) Compile(node parser.Node) (resultChunk *vm.Chunk, resultErrs hoistedGlobalNames = append(hoistedGlobalNames, name) } sort.Strings(hoistedGlobalNames) + // Eval code declares nothing unless every global function and var + // is definable (EvalDeclarationInstantiation steps 8 and 10, before + // any binding is created). + if (c.isIndirectEval || c.callerScopeDesc != nil) && !c.isStrictEvalCode() && c.enclosing == nil && !c.evalHoistsToEnv() { + for _, name := range hoistedGlobalNames { + if _, ok := program.HoistedDeclarations[name].(*parser.FunctionLiteral); ok { + globalIdx := c.GetOrAssignGlobalIndex(c.moduleGlobalKey(name)) + c.emitDeclareGlobal(globalIdx, name, vm.DeclareGlobalFunction|vm.DeclareGlobalCheck, 0, 0) + } + } + for _, name := range collectVarDeclarations(program.Statements) { + if _, isFunc := program.HoistedDeclarations[name]; isFunc { + continue + } + if _, _, found := c.currentSymbolTable.Resolve(name); found { + continue + } + globalIdx := c.GetOrAssignGlobalIndex(c.moduleGlobalKey(name)) + c.emitDeclareGlobal(globalIdx, name, vm.DeclareGlobalCheck, 0, 0) + } + } for _, name := range hoistedGlobalNames { hoistedNode := program.HoistedDeclarations[name] funcLit, ok := hoistedNode.(*parser.FunctionLiteral) @@ -1427,8 +1448,14 @@ func (c *Compiler) Compile(node parser.Node) (resultChunk *vm.Chunk, resultErrs // Update the symbol table entry to mark it as global c.currentSymbolTable.DefineGlobal(name, globalIdx) - // 5. Emit OpSetGlobal to store the function in the VM's globals array - c.emitSetGlobal(globalIdx, closureReg, funcLit.Token.Line) + // 5. Store the function in the VM's globals array. Eval code + // creates the binding with CreateGlobalFunctionBinding, which + // also redefines an existing configurable global property. + if c.isIndirectEval || c.callerScopeDesc != nil { + c.emitDeclareGlobal(globalIdx, name, vm.DeclareGlobalFunction|vm.DeclareGlobalDeletable, closureReg, funcLit.Token.Line) + } else { + c.emitSetGlobal(globalIdx, closureReg, funcLit.Token.Line) + } // 6. Mark as non-configurable (DontDelete) only for true top-level function declarations, // not for eval-created bindings which should be configurable @@ -1478,9 +1505,17 @@ func (c *Compiler) Compile(node parser.Node) (resultChunk *vm.Chunk, resultErrs // pre-existing non-writable global of the same name (e.g. `var // undefined = 42`, see #440 follow-up; OpSetGlobalInit resets // writability for exactly this reason). + // + // Eval code instead leaves an existing binding of the name + // alone (CreateGlobalVarBinding): `var x;` in eval must not reset + // the caller's x to undefined. tempReg := c.regAlloc.Alloc() - c.emitLoadUndefined(tempReg, 0) - c.emitSetGlobalInit(globalIdx, tempReg, 0) + if c.isIndirectEval || c.callerScopeDesc != nil { + c.emitDeclareGlobal(globalIdx, name, vm.DeclareGlobalDeletable, tempReg, 0) + } else { + c.emitLoadUndefined(tempReg, 0) + c.emitSetGlobalInit(globalIdx, tempReg, 0) + } c.regAlloc.Free(tempReg) // Mark as non-configurable (DontDelete) only for true top-level var declarations, // not for eval-created bindings which should be configurable diff --git a/pkg/compiler/emit.go b/pkg/compiler/emit.go index 5a77aa1e..cddfd832 100644 --- a/pkg/compiler/emit.go +++ b/pkg/compiler/emit.go @@ -754,6 +754,16 @@ func (c *Compiler) emitSetGlobalInit(globalIdx uint16, src Register, line int) { c.emitByte(byte(src)) } +// emitDeclareGlobal emits OpDeclareGlobal (flags: vm.DeclareGlobal*); src +// holds a function declaration's value. +func (c *Compiler) emitDeclareGlobal(globalIdx uint16, name string, flags byte, src Register, line int) { + c.emitOpCode(vm.OpDeclareGlobal, line) + c.emitUint16(globalIdx) + c.emitUint16(c.chunk.AddConstant(vm.String(name))) + c.emitByte(flags) + c.emitByte(byte(src)) +} + // emitToNumber implements unary plus by converting the operand to a number func (c *Compiler) emitToNumber(dest, src Register, line int) { c.emitOpCode(vm.OpToNumber, line) diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 35a15f17..1578eeb9 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -567,12 +567,6 @@ func (p *Paserati) IndirectEvalCode(code string) (vm.Value, []error) { return vm.Undefined, errs } - // For non-strict indirect eval, sync heap vars to GlobalObject - // This makes var declarations accessible via globalThis (ECMAScript requirement) - if !chunk.IsStrict { - p.vmInstance.SyncHeapToGlobalObject() - } - return result, nil } diff --git a/pkg/vm/bytecode.go b/pkg/vm/bytecode.go index aa51a2ba..13179d2b 100644 --- a/pkg/vm/bytecode.go +++ b/pkg/vm/bytecode.go @@ -335,6 +335,10 @@ const ( // OpTruncateWith drops the current frame's with objects beyond the first // N, for control leaving `with` bodies by break/continue or exception. OpTruncateWith OpCode = 185 // N + // OpDeclareGlobal creates an eval-declared global var or function binding + // (flags: DeclareGlobal*), leaving an existing var binding untouched and + // reflecting it on the global object; Ry is the function's value. + OpDeclareGlobal OpCode = 186 // GlobalIdx(16bit) NameIdx(16bit) Flags Ry // --- Accessor Property Support --- OpDefineAccessor OpCode = 80 // ObjReg GetterReg SetterReg NameIdx(16bit): Define accessor property on object @@ -676,6 +680,8 @@ func (op OpCode) String() string { return "OpCreateDeferredNamespace" case OpTruncateWith: return "OpTruncateWith" + case OpDeclareGlobal: + return "OpDeclareGlobal" // --- Arguments Object --- case OpGetArguments: @@ -1537,6 +1543,15 @@ func (c *Chunk) disassembleInstruction(builder *strings.Builder, offset int) int return c.registerConstantInstruction(builder, "OpCreateNamespace", offset, true) case OpCreateDeferredNamespace: return c.registerConstantInstruction(builder, "OpCreateDeferredNamespace", offset, true) + case OpDeclareGlobal: + if offset+6 >= len(c.Code) { + builder.WriteString("OpDeclareGlobal (missing operands)\n") + return offset + 1 + } + gi := uint16(c.Code[offset+1])<<8 | uint16(c.Code[offset+2]) + ni := uint16(c.Code[offset+3])<<8 | uint16(c.Code[offset+4]) + builder.WriteString(fmt.Sprintf("%-16s G%d %s flags=%d R%d\n", "OpDeclareGlobal", gi, c.Constants[ni].Inspect(), c.Code[offset+5], c.Code[offset+6])) + return offset + 7 // --- Arguments Object --- case OpGetArguments: diff --git a/pkg/vm/global_bindings.go b/pkg/vm/global_bindings.go new file mode 100644 index 00000000..da1e58f9 --- /dev/null +++ b/pkg/vm/global_bindings.go @@ -0,0 +1,174 @@ +package vm + +import "strings" + +// Global var and function bindings live in heap slots, but ECMAScript also +// makes them own data properties of the global object (the global +// environment's object record). Each such slot is mirrored onto the global +// object as an ordinary data property kept current by the heap's slot watch +// (see module_namespace.go), so descriptors, key listing and +// hasOwnProperty see it like any other property. + +// Flags of OpDeclareGlobal. +const ( + DeclareGlobalFunction = 1 << 0 // CreateGlobalFunctionBinding (else CreateGlobalVarBinding) + DeclareGlobalDeletable = 1 << 1 // eval code: the binding is configurable + DeclareGlobalCheck = 1 << 2 // only check CanDeclareGlobalFunction / CanDeclareGlobalVar +) + +// mirrorGlobalVar makes heap slot idx (global var name) an own data property +// of the global object kept in sync with the slot. A property the global +// object already has keeps its attributes; otherwise one is defined as +// writable, enumerable, and configurable only if deletable. +func (vm *VM) mirrorGlobalVar(idx int, name string, deletable bool) { + g := vm.GlobalObject + if g == nil || name == "" || strings.Contains(name, "\x00") { + return + } + val, ok := vm.heap.Get(idx) + if !ok || val.typ == TypeUninitialized { + val = Undefined + } + if _, exists := g.GetOwn(name); exists { + g.SetOwn(name, val) + } else { + w, e, c := true, true, deletable + g.DefineOwnProperty(name, val, &w, &e, &c) + } + vm.heap.watchGlobal(idx, g, name) +} + +// unmirrorGlobalVar drops the mirror of a deleted heap slot. +func (vm *VM) unmirrorGlobalVar(idx int) { + if vm.GlobalObject == nil { + return + } + if name := vm.heap.unwatchGlobal(idx, vm.GlobalObject); name != "" { + vm.GlobalObject.DeleteOwn(name) + } +} + +// reflectScriptVars mirrors a script's var and function declarations +// (chunk.VarGlobalIndices, already marked non-configurable) onto the global +// object. Module top-level bindings are not global object properties. +func (vm *VM) reflectScriptVars(chunk *Chunk) { + if chunk.IsModule || len(chunk.VarGlobalIndices) == 0 || vm.GlobalObject == nil { + return + } + isVar := make(map[int]bool, len(chunk.VarGlobalIndices)) + for _, idx := range chunk.VarGlobalIndices { + isVar[int(idx)] = true + } + for name, idx := range vm.heap.GetNameToIndex() { + if isVar[idx] { + vm.mirrorGlobalVar(idx, name, false) + } + } +} + +// canDeclareGlobalFunction is CanDeclareGlobalFunction (ECMA-262 +// 9.1.1.4.16): an existing non-configurable property must be a writable, +// enumerable data property. +func (vm *VM) canDeclareGlobalFunction(name string) bool { + g := vm.GlobalObject + if g == nil { + return true + } + i := g.shape.lookupStringField(name) + if i < 0 { + return g.IsExtensible() + } + f := g.shape.fields[i] + return f.configurable || (!f.isAccessor && f.writable && f.enumerable) +} + +// canDeclareGlobalVar is CanDeclareGlobalVar (ECMA-262 9.1.1.4.15). +func (vm *VM) canDeclareGlobalVar(idx int, name string) bool { + g := vm.GlobalObject + if g == nil || vm.heap.IsInitialized(idx) { + return true + } + if _, exists := g.GetOwn(name); exists { + return true + } + return g.IsExtensible() +} + +// declareGlobal implements CreateGlobalVarBinding and +// CreateGlobalFunctionBinding (ECMA-262 9.1.1.4.17-18) for eval code, where +// the binding may already exist - as an earlier declaration's heap slot or +// as a property of the global object. +func (vm *VM) declareGlobal(idx int, name string, flags byte, value Value) { + h := vm.heap + g := vm.GlobalObject + deletable := flags&DeclareGlobalDeletable != 0 + if flags&DeclareGlobalFunction == 0 { + // An existing binding keeps its value and attributes. + if h.IsInitialized(idx) { + vm.mirrorGlobalVar(idx, name, deletable) + return + } + if g != nil { + if v, exists := g.GetOwn(name); exists { + _ = h.Set(idx, v) + vm.mirrorGlobalVar(idx, name, deletable) + return + } + } + _ = h.Set(idx, Undefined) + _ = h.SetConfigurable(idx, deletable) + _ = h.SetWritable(idx, true) + vm.mirrorGlobalVar(idx, name, deletable) + return + } + // A function replaces the binding's value; an existing non-configurable + // property also keeps its attributes, anything else is redefined. + redefine := true + if g != nil { + if i := g.shape.lookupStringField(name); i >= 0 && !g.shape.fields[i].configurable { + redefine = false + } + } + if redefine { + _ = h.SetConfigurable(idx, deletable) + _ = h.SetWritable(idx, true) + if g != nil { + w, e, c := true, true, deletable + g.DefineOwnProperty(name, value, &w, &e, &c) + } + } + _ = h.Set(idx, value) + vm.mirrorGlobalVar(idx, name, deletable) +} + +// watchGlobal registers the global object property name to mirror slot +// index, once. +func (h *Heap) watchGlobal(index int, g *PlainObject, name string) { + if index >= 0 && index < len(h.watch) { + for _, b := range h.watch[index] { + if b.ns == g && b.name == name { + return + } + } + } + h.watchSlot(index, namespaceBinding{ns: g, name: name}) +} + +// unwatchGlobal removes slot index's mirror on g, returning its name. +func (h *Heap) unwatchGlobal(index int, g *PlainObject) string { + if index < 0 || index >= len(h.watch) { + return "" + } + bs := h.watch[index] + for i, b := range bs { + if b.ns == g { + h.watch[index] = append(bs[:i:i], bs[i+1:]...) + return b.name + } + } + return "" +} + +// UnmirrorGlobalVar is unmirrorGlobalVar for builtins deleting a global +// object property backed by heap slot idx. +func (vm *VM) UnmirrorGlobalVar(idx int) { vm.unmirrorGlobalVar(idx) } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index fc1bd418..0d608fd4 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1755,6 +1755,7 @@ func (vm *VM) Interpret(chunk *Chunk) (Value, []errors.PaseratiError) { _ = vm.heap.SetConfigurable(int(idx), false) // After resize, this should succeed } } + vm.reflectScriptVars(chunk) // Run the VM // fmt.Printf("// [VM] Interpret: About to call vm.run() for chunk '%s' (stack depth: %d)\n", mainFuncObj.Name, len(vm.executionContextStack)) @@ -14503,6 +14504,11 @@ startExecution: } vm.setGlobalInTable(globalIdx, value) + // A sloppy assignment to an unresolvable name creates a + // configurable property of the global object. + if !heapExists && !function.Chunk.IsModule { + vm.mirrorGlobalVar(int(globalIdx), vm.heap.GetNameByIndex(int(globalIdx)), true) + } // Debug output (disabled) // if globalIdx == 21 { @@ -17081,7 +17087,9 @@ startExecution: // If this is the GlobalObject, also mark the heap entry as deleted if success && po == vm.GlobalObject { if idx, exists := vm.heap.GetNameToIndex()[propName]; exists { - vm.heap.Delete(idx) + if vm.heap.Delete(idx) { + vm.unmirrorGlobalVar(idx) + } } } } else if obj.Type() == TypeDictObject { @@ -17619,7 +17627,9 @@ startExecution: // GlobalThis special case: keep heap in sync if success && po == vm.GlobalObject { if idx, exists := vm.heap.GetNameToIndex()[propName]; exists { - vm.heap.Delete(idx) + if vm.heap.Delete(idx) { + vm.unmirrorGlobalVar(idx) + } } } } @@ -17826,8 +17836,32 @@ startExecution: // Try to delete from heap - returns false if non-configurable success := vm.heap.Delete(int(heapIdx)) + if success { + vm.unmirrorGlobalVar(int(heapIdx)) + } registers[destReg] = BooleanValue(success) + case OpDeclareGlobal: + globalIdx := int(uint16(code[ip])<<8 | uint16(code[ip+1])) + nameIdx := int(uint16(code[ip+2])<<8 | uint16(code[ip+3])) + flags := code[ip+4] + srcReg := code[ip+5] + ip += 6 + name := constants[nameIdx].AsString() + if flags&DeclareGlobalCheck != 0 { + isFunc := flags&DeclareGlobalFunction != 0 + if (isFunc && !vm.canDeclareGlobalFunction(name)) || (!isFunc && !vm.canDeclareGlobalVar(globalIdx, name)) { + frame.ip = ip + vm.ThrowTypeError(fmt.Sprintf("Cannot declare global binding '%s'", name)) + if vm.unwinding { + return InterpretRuntimeError, Undefined + } + goto reloadFrame + } + continue + } + vm.declareGlobal(globalIdx, name, flags, registers[srcReg]) + case OpToPropertyKey: // OpToPropertyKey: Rx Ry: Rx = ToPropertyKey(Ry) // Converts value to property key, calling toString() method for objects diff --git a/tests/scripts/global_var_bindings_on_global_object.ts b/tests/scripts/global_var_bindings_on_global_object.ts new file mode 100644 index 00000000..48620e19 --- /dev/null +++ b/tests/scripts/global_var_bindings_on_global_object.ts @@ -0,0 +1,31 @@ +// Global var/function bindings are data properties of the global object +// (non-configurable for script declarations, configurable for eval-created +// ones and implicit globals); let/const/class bindings are not; a var +// redeclared in eval keeps its value +// no-typecheck +// expect: v:w/e/-|f:w/e/-|ev:w/e/c|ef:w/e/c|iv:w/e/c|zz:w/e/c|let:none|class:none|23,7,function + +var v = 1; +function f() {} +let l = 1; +class K {} +eval("var ev = 1; function ef() {}"); +(0, eval)("var iv = 1;"); +zz = 1; + +function attrs(name) { + var d = Object.getOwnPropertyDescriptor(globalThis, name); + if (!d) return name + ":none"; + return name + ":" + (d.writable ? "w" : "-") + "/" + (d.enumerable ? "e" : "-") + "/" + (d.configurable ? "c" : "-"); +} + +var x = 23; +eval("var x;"); +var y = 7; +(0, eval)("var y;"); +(0, eval)("var f;"); + +[attrs("v"), attrs("f"), attrs("ev"), attrs("ef"), attrs("iv"), attrs("zz"), + "let:" + (globalThis.hasOwnProperty("l") ? "own" : "none"), + "class:" + (globalThis.hasOwnProperty("K") ? "own" : "none"), + [x, y, typeof f].join(",")].join("|");