Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions pkg/builtins/object_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pkg/builtins/reflect_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/compiler/compile_class.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
43 changes: 39 additions & 4 deletions pkg/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pkg/compiler/emit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 0 additions & 6 deletions pkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/vm/bytecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -676,6 +680,8 @@ func (op OpCode) String() string {
return "OpCreateDeferredNamespace"
case OpTruncateWith:
return "OpTruncateWith"
case OpDeclareGlobal:
return "OpDeclareGlobal"

// --- Arguments Object ---
case OpGetArguments:
Expand Down Expand Up @@ -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:
Expand Down
174 changes: 174 additions & 0 deletions pkg/vm/global_bindings.go
Original file line number Diff line number Diff line change
@@ -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) }
Loading
Loading