// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. package hbrt import "strings" // pendingCall stores the symbol for the next Function/Do call. // This avoids storing Go pointers in Value.data (which GC can't trace). // PushSymbol records the function symbol for the next call. // The actual symbol is stored in Thread, not on the eval stack. // A marker NIL is pushed to keep stack positions correct. // Harbour: hb_xvmPushSymbol func (t *Thread) PushSymbol(sym *Symbol) { t.pushPendingSym(sym) t.push(MakeNil()) // placeholder for symbol position } // Function calls the function with nArgs arguments. // Stack layout before: [sym_placeholder] [nil/self] [arg1] ... [argN] // Stack after: [retval] // Harbour: hb_xvmFunction func (t *Thread) Function(nArgs int) { sym := t.popPendingSym() if sym == nil { panic(t.runtimeError("no function symbol for call")) } // Resolve function. First call for an external/lazy symbol misses // sym.Func and walks the VM symbol table — cache the resolved Func // back into the Symbol so subsequent calls skip the ToUpper + // RWMutex + map lookup. Symbols are shared read-mostly so a racy // write is safe (both racers resolve to the same Func pointer). fn := sym.Func if fn == nil && t.vm != nil { found := t.vm.FindSymbol(strings.ToUpper(sym.Name)) if found != nil { fn = found.Func sym.Func = fn } } if fn == nil { panic(t.runtimeError("undefined function: " + sym.Name)) } // Stack at entry (bottom → top): // [sym placeholder] [self/NIL] [arg1] … [argN] // Frame() expects only [arg1..argN] on the eval stack so it can // copy them into the callee's locals. The old code achieved this // by pop-popping args, popping the two placeholders, then pushing // the args back — an O(N) copy plus a heap allocation per call. // Shift the args two slots left in place instead: one slice move, // zero heap. if nArgs > 0 { base := t.sp - nArgs - 2 copy(t.stack[base:base+nArgs], t.stack[t.sp-nArgs:t.sp]) } // Two slots freed at top — keep them nil so the GC can release any // references they held (matches pop()'s clearing semantics). t.stack[t.sp-1] = cachedNil t.stack[t.sp-2] = cachedNil t.sp -= 2 // Set pending params count and symbol for Frame() t.pendingParams = nArgs t.pendingCallSym = sym // Call fn(t) // Push return value t.push(t.retVal) } // Do calls the function but discards the return value. // Harbour: hb_xvmDo func (t *Thread) Do(nArgs int) { t.Function(nArgs) t.pop() // discard return value }