Variadic Harbour functions (`FUNCTION foo(...)`) declare 0 params,
so Frame() copied no args into the locals slots PValue/CallerLocal
read from. Result: PValue(n) returned whatever happened to live in
the caller's LOCAL slot n (i.e. the first LOCAL declared after `(...)`,
which loops typically use as a counter). fivenode's AP_RPUTS, AP_ECHO
and any other variadic dispatcher that walks PValue() saw garbage —
fivenode_go shipped a workaround (AP_RPUTS collapsed to a single arg)
that this commit now lets us revert.
Mechanism
CallFrame now carries `actualArgs []Value`. Frame() snapshots every
value the caller pushed (the full pendingParams, not the clipped
declared count) into this slice before moving sp. The locals[]
declared-param region is unchanged so positional LOCAL access keeps
working. CallerLocal reads from actualArgs first.
Stack handling tightens slightly: t.sp now ends at `sp - pushedArgs`
instead of `sp - localsCopy`, dropping the extra-args slots that
variadic callers used to leave on the stack. They're no longer needed
— actualArgs is the canonical home — and leaving them on the stack
was the root of the original "PValue returns the caller's LOCAL"
bug because the next push would overwrite them.
Slice reuse: when capacity permits, we slice down rather than
reallocating, so the hot path (0/1/few args) keeps its no-alloc
characteristics.
Verified
• Variadic 2/1/0-arg case and fixed-arg comparison all print the
expected values (test_variadic_only.prg).
• Full suite: go test ./compiler/... ./hbrt/... ./hbrtl/...,
Compat 56/56, std.ch 17/17, FRB 7/7, FiveSql2 43/43 all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
855 lines
24 KiB
Go
855 lines
24 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
package hbrt
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// Default stack/frame sizes
|
|
const (
|
|
DefaultStackSize = 2048 // initial eval stack capacity
|
|
MaxStackSize = 65536
|
|
MaxCallDepth = 256
|
|
InitialCallDepth = 32 // start small, grow if needed
|
|
)
|
|
|
|
// CallFrame saves the state of a function call.
|
|
// Harbour equivalent: HB_STACK_STATE
|
|
type CallFrame struct {
|
|
symbol *Symbol // function symbol (for debugging/profiling)
|
|
base int // stack base (start of this frame's args)
|
|
localBase int // where locals start in the locals slice
|
|
localCount int // number of locals in this frame
|
|
paramCount int // number of parameters passed
|
|
retVal Value // return value
|
|
module string // current PRG source file (updated by DebugLine)
|
|
line int // current PRG source line
|
|
localNames []string // PRG-source names of params+locals (nil = none registered)
|
|
// actualArgs snapshots every value the caller pushed, regardless of
|
|
// the callee's declared param count. PValue / CallerLocal read this
|
|
// so variadic functions (declared params = 0) can see their args —
|
|
// the locals[] slots are reserved for declared LOCALs in that case,
|
|
// so the args have nowhere else to live. Reused across calls when
|
|
// capacity allows.
|
|
actualArgs []Value
|
|
}
|
|
|
|
// CurFrame returns the current call frame (for closure capture).
|
|
func (t *Thread) CurFrame() *CallFrame { return t.curFrame }
|
|
|
|
// CurBlock returns the *HbBlock for the codeblock currently executing
|
|
// (or nil outside a block body). Used by pcode dispatch to resolve
|
|
// detached-local opcodes against the running block's capture slice.
|
|
func (t *Thread) CurBlock() *HbBlock { return t.curBlock }
|
|
|
|
// SetCurBlock installs the executing block. The pcode block wrapper
|
|
// pairs Set(block) with a deferred Set(prev) to nest correctly across
|
|
// blocks that call other blocks (`{|| eval(b1) + eval(b2) }`).
|
|
func (t *Thread) SetCurBlock(b *HbBlock) { t.curBlock = b }
|
|
|
|
// LocalsSlice returns the underlying locals array (for closure capture).
|
|
func (t *Thread) LocalsSlice() []Value { return t.locals }
|
|
|
|
// GetLocal reads a local variable from a captured frame (1-based index).
|
|
func (f *CallFrame) GetLocal(n int, locals []Value) Value {
|
|
idx := f.localBase + n - 1
|
|
return locals[idx]
|
|
}
|
|
|
|
// SetLocal writes a local variable in a captured frame (1-based index).
|
|
func (f *CallFrame) SetLocal(n int, v Value, locals []Value) {
|
|
idx := f.localBase + n - 1
|
|
locals[idx] = v
|
|
}
|
|
|
|
// Thread is the per-goroutine execution context.
|
|
// Harbour equivalent: HB_STACK (thread-local stack)
|
|
//
|
|
// Each goroutine that runs Harbour code gets its own Thread.
|
|
// No locking needed for stack/locals/calls — they are goroutine-local.
|
|
// The TID is VM-unique and assigned at construction time for debugger
|
|
// thread listing.
|
|
// TraceEntry captures one step of execution history — module+line where
|
|
// DebugLine fired. Populated only when the debugger is attached so
|
|
// regular runs don't pay the ring-buffer cost.
|
|
type TraceEntry struct {
|
|
Module string
|
|
Line int
|
|
}
|
|
|
|
// Size of the per-thread execution trace ring buffer. 256 entries gives
|
|
// enough runway to answer "how did we get here?" across most loops
|
|
// without meaningfully bloating per-thread memory.
|
|
const TraceRingSize = 256
|
|
|
|
type Thread struct {
|
|
tid uint32
|
|
|
|
// traceRing is a ring buffer of recent DebugLine hits. traceHead
|
|
// points at the slot for the next write. Total recorded entries
|
|
// across the program's lifetime for this thread is tracked via
|
|
// traceCount so the debugger can render "N lines ago".
|
|
traceRing []TraceEntry
|
|
traceHead int
|
|
traceCount uint64
|
|
// Eval stack (goroutine-local, no lock needed)
|
|
stack []Value
|
|
sp int // stack pointer (next free slot)
|
|
|
|
// Local variables: flat array, each frame gets a slice via localBase+localCount
|
|
locals []Value
|
|
|
|
// Call stack
|
|
calls []CallFrame
|
|
callSP int // call stack pointer
|
|
curFrame *CallFrame
|
|
|
|
// Return value (passed between caller/callee)
|
|
retVal Value
|
|
|
|
// Pending function call stack (PushSymbol pushes, Function pops).
|
|
// Depth=1 is the common case (non-nested call) and gets a scalar
|
|
// fast slot to skip slice append/trim; nested calls fall back to
|
|
// the heap slice. Balanced push/pop keeps the invariant:
|
|
// pendingSymFast set → slice empty
|
|
// slice non-empty → pendingSymFast may or may not be set.
|
|
pendingSymFast *Symbol
|
|
pendingSyms []*Symbol
|
|
pendingParams int // number of params for next Frame call
|
|
pendingCallSym *Symbol // symbol for next Frame (for PROCNAME)
|
|
|
|
// STATIC variables (per-module, shared but rarely written)
|
|
// Accessed via PushStatic/PopStatic with module reference
|
|
statics map[string][]Value
|
|
|
|
// OOP: current Self object (set during method dispatch)
|
|
self Value
|
|
|
|
// Current code block (set while a block body is executing).
|
|
// Pcode opcodes PcOpPushDetached / PcOpPopDetached read/write
|
|
// Detached[i] through this pointer. The block's wrapper Fn
|
|
// sets it before ExecPcode and restores on exit. Stays nil
|
|
// outside block bodies; nil-checking opcodes fall back to NIL.
|
|
curBlock *HbBlock
|
|
|
|
// Error handling: last error from BEGIN SEQUENCE
|
|
lastError *HbError
|
|
|
|
// MEMVAR: PUBLIC/PRIVATE variables (shared across call stack)
|
|
Memvars *MemvarTable
|
|
|
|
// WorkArea manager (goroutine-local, no locks needed)
|
|
WA interface{} // *hbrdd.WorkAreaManager — set by caller to avoid import cycle
|
|
|
|
// FastFieldGetter is a hot-path closure set by SqlScan (or any other
|
|
// scan loop) to short-circuit PcOpFieldGet. When non-nil, the pcode
|
|
// interpreter calls this instead of going through PushSymbol +
|
|
// Function dispatch + FieldGet RTL's own Frame/EndProc. Caller is
|
|
// responsible for setting and clearing it around a scan.
|
|
FastFieldGetter func(int) Value
|
|
waStack []uint16 // saved workarea numbers for (expr)->(expr) context switching
|
|
|
|
// VM reference (shared, read-mostly)
|
|
vm *VM
|
|
}
|
|
|
|
// NewThread creates a new execution thread.
|
|
func NewThread(vm *VM) *Thread {
|
|
t := &Thread{
|
|
stack: make([]Value, DefaultStackSize),
|
|
sp: 0,
|
|
locals: make([]Value, 256), // will grow as needed
|
|
calls: make([]CallFrame, InitialCallDepth),
|
|
callSP: 0,
|
|
statics: make(map[string][]Value),
|
|
Memvars: NewMemvarTable(),
|
|
vm: vm,
|
|
}
|
|
return t
|
|
}
|
|
|
|
// --- Stack operations ---
|
|
|
|
func (t *Thread) push(v Value) {
|
|
if t.sp >= len(t.stack) {
|
|
if t.sp >= MaxStackSize {
|
|
panic(t.runtimeError("stack overflow"))
|
|
}
|
|
newStack := make([]Value, len(t.stack)*2)
|
|
copy(newStack, t.stack[:t.sp])
|
|
t.stack = newStack
|
|
}
|
|
t.stack[t.sp] = v
|
|
t.sp++
|
|
}
|
|
|
|
func (t *Thread) pop() Value {
|
|
if t.sp <= 0 {
|
|
panic(t.runtimeError("stack underflow"))
|
|
}
|
|
t.sp--
|
|
v := t.stack[t.sp]
|
|
t.stack[t.sp] = cachedNil
|
|
return v
|
|
}
|
|
|
|
func (t *Thread) peek() Value {
|
|
if t.sp <= 0 {
|
|
panic(t.runtimeError("stack underflow (peek)"))
|
|
}
|
|
return t.stack[t.sp-1]
|
|
}
|
|
|
|
func (t *Thread) peekPtr() *Value {
|
|
if t.sp <= 0 {
|
|
panic(t.runtimeError("stack underflow (peekPtr)"))
|
|
}
|
|
return &t.stack[t.sp-1]
|
|
}
|
|
|
|
func (t *Thread) setTop(v Value) {
|
|
if t.sp <= 0 {
|
|
panic(t.runtimeError("stack underflow (setTop)"))
|
|
}
|
|
t.stack[t.sp-1] = v
|
|
}
|
|
|
|
// stackAt returns a pointer to stack item at offset from top.
|
|
// 0 = top, -1 = second from top, etc.
|
|
func (t *Thread) stackAt(offset int) *Value {
|
|
idx := t.sp - 1 + offset
|
|
if idx < 0 || idx >= t.sp {
|
|
panic(t.runtimeError("stack access out of range"))
|
|
}
|
|
return &t.stack[idx]
|
|
}
|
|
|
|
// --- Push convenience methods (used by generated code) ---
|
|
|
|
func (t *Thread) PushNil() { t.push(MakeNil()) }
|
|
func (t *Thread) PushBool(b bool) { t.push(MakeBool(b)) }
|
|
func (t *Thread) PushInt(n int) { t.push(MakeInt(n)) }
|
|
func (t *Thread) PushLong(n int64) { t.push(MakeLong(n)) }
|
|
func (t *Thread) PushDouble(v float64, length, decimal uint16) {
|
|
t.push(MakeDouble(v, length, decimal))
|
|
}
|
|
func (t *Thread) PushString(s string) { t.push(MakeString(s)) }
|
|
func (t *Thread) PushValue(v Value) { t.push(v) }
|
|
|
|
func (t *Thread) Pop() { t.pop() }
|
|
func (t *Thread) Pop2() Value { return t.pop() } // pop and return
|
|
func (t *Thread) Dup() { t.push(t.peek()) }
|
|
|
|
// SP returns the current data-stack depth. Paired with SetSP for
|
|
// callers that need to clamp the stack across an inner function
|
|
// dispatch — used by FrbDo to neutralise pcode-body stack leaks.
|
|
func (t *Thread) SP() int { return t.sp }
|
|
|
|
// SetSP forcibly resets the data-stack depth. Truncates if newSP < sp;
|
|
// extends with NIL if newSP > sp (defensive — should never grow here
|
|
// in practice). Bounds-checked against the underlying slice so a
|
|
// negative or out-of-range value can't corrupt the runtime.
|
|
func (t *Thread) SetSP(newSP int) {
|
|
if newSP < 0 {
|
|
newSP = 0
|
|
}
|
|
if newSP > len(t.stack) {
|
|
newSP = len(t.stack)
|
|
}
|
|
// Clear any slots being abandoned so stale values can't surface
|
|
// later through Dup/peek paths.
|
|
for i := newSP; i < t.sp; i++ {
|
|
t.stack[i] = cachedNil
|
|
}
|
|
t.sp = newSP
|
|
}
|
|
|
|
// --- Frame management ---
|
|
// Harbour: hb_xvmFrame(params, locals)
|
|
// Called at the start of every function.
|
|
|
|
func (t *Thread) Frame(params, locals int) {
|
|
if t.callSP >= MaxCallDepth {
|
|
panic(t.runtimeError("call stack overflow"))
|
|
}
|
|
// Grow call stack dynamically if needed
|
|
if t.callSP >= len(t.calls) {
|
|
newSize := len(t.calls) * 2
|
|
if newSize > MaxCallDepth {
|
|
newSize = MaxCallDepth
|
|
}
|
|
newCalls := make([]CallFrame, newSize)
|
|
copy(newCalls, t.calls)
|
|
t.calls = newCalls
|
|
}
|
|
|
|
// Ensure locals slice has enough space
|
|
localBase := 0
|
|
if t.curFrame != nil {
|
|
localBase = t.curFrame.localBase + t.curFrame.localCount
|
|
}
|
|
needed := localBase + params + locals
|
|
if needed > len(t.locals) {
|
|
newLocals := make([]Value, needed*2)
|
|
copy(newLocals, t.locals)
|
|
t.locals = newLocals
|
|
}
|
|
|
|
// Save frame
|
|
// pushedArgs is the actual count the caller put on the stack.
|
|
// localsCopy is how many of those map onto the callee's declared
|
|
// LOCAL slot indexes (clipped at the declared param count).
|
|
pushedArgs := t.pendingParams
|
|
if pushedArgs > t.sp {
|
|
pushedArgs = t.sp
|
|
}
|
|
localsCopy := pushedArgs
|
|
if localsCopy > params {
|
|
localsCopy = params
|
|
}
|
|
|
|
frame := &t.calls[t.callSP]
|
|
frame.base = t.sp - localsCopy // unchanged: only declared args bind to LOCAL slots
|
|
frame.localBase = localBase
|
|
frame.localCount = params + locals
|
|
frame.paramCount = t.pendingParams // actual args passed by caller (not declared count)
|
|
frame.retVal = MakeNil()
|
|
frame.symbol = t.pendingCallSym
|
|
t.pendingCallSym = nil
|
|
|
|
// Snapshot every pushed arg BEFORE we move sp — extras would be
|
|
// overwritten as soon as this frame's body started pushing for its
|
|
// own expression evaluation. Reuse the slice when capacity allows
|
|
// to keep the no-arg / few-arg path allocation-free.
|
|
if pushedArgs > 0 {
|
|
snapStart := t.sp - pushedArgs
|
|
if cap(frame.actualArgs) >= pushedArgs {
|
|
frame.actualArgs = frame.actualArgs[:pushedArgs]
|
|
} else {
|
|
frame.actualArgs = make([]Value, pushedArgs)
|
|
}
|
|
copy(frame.actualArgs, t.stack[snapStart:snapStart+pushedArgs])
|
|
} else if frame.actualArgs != nil {
|
|
frame.actualArgs = frame.actualArgs[:0]
|
|
}
|
|
|
|
// Copy actual parameters from stack to locals (declared-param portion).
|
|
for i := 0; i < localsCopy; i++ {
|
|
t.locals[localBase+i] = t.stack[frame.base+i]
|
|
}
|
|
|
|
// Initialize missing params and locals to NIL
|
|
for i := localsCopy; i < params+locals; i++ {
|
|
t.locals[localBase+i] = MakeNil()
|
|
}
|
|
|
|
// Pop args from stack (they're now in locals + actualArgs).
|
|
// Extras beyond declared params (variadic) live only in actualArgs;
|
|
// drop the stack slots so the caller can't accidentally re-read.
|
|
t.sp = t.sp - pushedArgs
|
|
|
|
t.curFrame = frame
|
|
t.callSP++
|
|
}
|
|
|
|
// EndProc is called via defer at the end of every function.
|
|
// Handles recover for BEGIN SEQUENCE and restores frame.
|
|
// All panics are re-panicked so the generated SEQUENCE/RECOVER handler
|
|
// can catch them. HbError + BreakValue (from Break() in hbrtl) are
|
|
// re-panicked silently; unknown panics also re-panic but with a
|
|
// diagnostic message on stderr.
|
|
func (t *Thread) EndProc() {
|
|
if r := recover(); r != nil {
|
|
t.endFrame()
|
|
if _, ok := r.(*HbError); ok {
|
|
panic(r) // HbError — re-panic silently
|
|
}
|
|
// Check for BreakValue from hbrtl.Break() via duck typing.
|
|
// We can't import hbrtl (cycle), so we check the type name.
|
|
rType := fmt.Sprintf("%T", r)
|
|
if rType == "hbrtl.BreakValue" {
|
|
panic(r) // BreakValue — re-panic silently for RECOVER USING
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Five runtime error: %v [recovered, repanicked]\n", r)
|
|
panic(r)
|
|
}
|
|
t.endFrame()
|
|
}
|
|
|
|
// EndProcFast is called by RTL functions that don't need recover().
|
|
// ~3x faster than EndProc (no defer recover overhead).
|
|
func (t *Thread) EndProcFast() {
|
|
t.endFrame()
|
|
}
|
|
|
|
// endFrame restores the previous call frame.
|
|
func (t *Thread) endFrame() {
|
|
if t.callSP > 0 {
|
|
t.callSP--
|
|
if t.callSP > 0 {
|
|
t.curFrame = &t.calls[t.callSP-1]
|
|
} else {
|
|
t.curFrame = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// EndProcNoRecover cleans up the frame without recover (used by Break).
|
|
func (t *Thread) EndProcNoRecover() {
|
|
if t.callSP > 0 {
|
|
t.callSP--
|
|
if t.callSP > 0 {
|
|
t.curFrame = &t.calls[t.callSP-1]
|
|
} else {
|
|
t.curFrame = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Local variable access ---
|
|
// Harbour convention: local index 1-based (1 = first param or local)
|
|
|
|
func (t *Thread) PushLocal(n int) {
|
|
idx := t.localIndex(n)
|
|
v := t.locals[idx]
|
|
if v.Type() == tByref {
|
|
t.push((*HbRefCell)(v.ptr).V)
|
|
} else {
|
|
t.push(v)
|
|
}
|
|
}
|
|
|
|
func (t *Thread) PopLocal(n int) {
|
|
idx := t.localIndex(n)
|
|
val := t.pop()
|
|
if e := t.locals[idx]; e.Type() == tByref {
|
|
(*HbRefCell)(e.ptr).V = val
|
|
} else {
|
|
t.locals[idx] = val
|
|
}
|
|
}
|
|
|
|
func (t *Thread) Local(n int) Value {
|
|
v := t.locals[t.localIndex(n)]
|
|
if v.Type() == tByref {
|
|
return (*HbRefCell)(v.ptr).V
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (t *Thread) SetLocal(n int, v Value) {
|
|
idx := t.localIndex(n)
|
|
if e := t.locals[idx]; e.Type() == tByref {
|
|
(*HbRefCell)(e.ptr).V = v
|
|
} else {
|
|
t.locals[idx] = v
|
|
}
|
|
}
|
|
|
|
// Fast variants — no bounds checking (gengo guarantees valid indices).
|
|
// Byref-aware: transparently dereference/write-through RefCell.
|
|
|
|
func (t *Thread) PushLocalFast(n int) {
|
|
v := t.locals[t.curFrame.localBase+n-1]
|
|
if v.Type() == tByref {
|
|
t.push((*HbRefCell)(v.ptr).V)
|
|
} else {
|
|
t.push(v)
|
|
}
|
|
}
|
|
|
|
func (t *Thread) PopLocalFast(n int) {
|
|
idx := t.curFrame.localBase + n - 1
|
|
val := t.pop()
|
|
if e := t.locals[idx]; e.Type() == tByref {
|
|
(*HbRefCell)(e.ptr).V = val
|
|
} else {
|
|
t.locals[idx] = val
|
|
}
|
|
}
|
|
|
|
func (t *Thread) LocalFast(n int) Value {
|
|
v := t.locals[t.curFrame.localBase+n-1]
|
|
if v.Type() == tByref {
|
|
return (*HbRefCell)(v.ptr).V
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (t *Thread) SetLocalFast(n int, v Value) {
|
|
idx := t.curFrame.localBase + n - 1
|
|
if e := t.locals[idx]; e.Type() == tByref {
|
|
(*HbRefCell)(e.ptr).V = v
|
|
} else {
|
|
t.locals[idx] = v
|
|
}
|
|
}
|
|
|
|
// PushLocalRef creates a shared RefCell and pushes it for @param.
|
|
// Both caller's local and callee's param point to the same cell.
|
|
func (t *Thread) PushLocalRef(n int) {
|
|
idx := t.localIndex(n)
|
|
v := t.locals[idx]
|
|
if v.Type() == tByref {
|
|
t.push(v) // already a RefCell — share it
|
|
return
|
|
}
|
|
cell := &HbRefCell{V: v}
|
|
ref := MakeByref(cell)
|
|
t.locals[idx] = ref // caller's local becomes RefCell
|
|
t.push(ref) // callee gets same RefCell
|
|
}
|
|
|
|
func (t *Thread) LocalAsString(n int) string {
|
|
return t.Local(n).AsString()
|
|
}
|
|
|
|
// EnsureLocalRef converts a local to a RefCell if it isn't one already.
|
|
// Used by closure capture to enable shared mutable access.
|
|
func (t *Thread) EnsureLocalRef(n int) {
|
|
idx := t.curFrame.localBase + n - 1
|
|
v := t.locals[idx]
|
|
if v.Type() != tByref {
|
|
cell := &HbRefCell{V: v}
|
|
t.locals[idx] = MakeByref(cell)
|
|
}
|
|
}
|
|
|
|
// LocalRaw returns the raw Value at local slot (including RefCell wrapper).
|
|
// Used by closure capture to grab the RefCell itself, not the dereferenced value.
|
|
func (t *Thread) LocalRaw(n int) Value {
|
|
return t.locals[t.curFrame.localBase+n-1]
|
|
}
|
|
|
|
// SetLocalRaw sets a local slot to the raw Value (including RefCell wrapper).
|
|
// Used by closure to inject shared RefCell into block locals.
|
|
func (t *Thread) SetLocalRaw(n int, v Value) {
|
|
t.locals[t.curFrame.localBase+n-1] = v
|
|
}
|
|
|
|
// LocalSetInt is an optimization: set local directly without stack. Byref-aware.
|
|
func (t *Thread) LocalSetInt(n int, val int) {
|
|
idx := t.localIndex(n)
|
|
if e := t.locals[idx]; e.Type() == tByref {
|
|
(*HbRefCell)(e.ptr).V = MakeInt(val)
|
|
} else {
|
|
t.locals[idx] = MakeInt(val)
|
|
}
|
|
}
|
|
|
|
func (t *Thread) localIndex(n int) int {
|
|
if t.curFrame == nil {
|
|
panic(t.runtimeError("no active frame"))
|
|
}
|
|
idx := t.curFrame.localBase + n - 1 // 1-based to 0-based
|
|
if idx < t.curFrame.localBase || idx >= t.curFrame.localBase+t.curFrame.localCount {
|
|
panic(t.runtimeError(fmt.Sprintf("local variable index out of range: %d", n)))
|
|
}
|
|
return idx
|
|
}
|
|
|
|
// --- Memvar access ---
|
|
|
|
// PushMemvar pushes a memvar value onto the stack. Harbour: M->varname
|
|
func (t *Thread) PushMemvar(name string) {
|
|
if v, ok := t.Memvars.Get(name); ok {
|
|
t.push(v)
|
|
} else {
|
|
t.push(MakeNil())
|
|
}
|
|
}
|
|
|
|
// PopMemvar pops stack and stores into a memvar. Harbour: M->varname := expr
|
|
func (t *Thread) PopMemvar(name string) {
|
|
val := t.pop()
|
|
if !t.Memvars.Set(name, val) {
|
|
// Auto-create as PRIVATE if not exists
|
|
t.Memvars.SetPrivate(name, val, t.callSP)
|
|
}
|
|
}
|
|
|
|
// DeclarePublic creates a PUBLIC memvar with NIL value.
|
|
func (t *Thread) DeclarePublic(name string) {
|
|
if !t.Memvars.Exists(name) {
|
|
t.Memvars.SetPublic(name, MakeNil())
|
|
}
|
|
}
|
|
|
|
// DeclarePrivate creates a PRIVATE memvar with NIL value.
|
|
func (t *Thread) DeclarePrivate(name string) {
|
|
t.Memvars.SetPrivate(name, MakeNil(), t.callSP)
|
|
}
|
|
|
|
// --- Return value ---
|
|
|
|
func (t *Thread) RetValue() {
|
|
t.retVal = t.pop()
|
|
}
|
|
|
|
func (t *Thread) RetInt(n int64) {
|
|
t.retVal = MakeNumInt(n)
|
|
}
|
|
|
|
func (t *Thread) RetNil() {
|
|
t.retVal = MakeNil()
|
|
}
|
|
|
|
func (t *Thread) RetString(s string) {
|
|
t.retVal = MakeString(s)
|
|
}
|
|
|
|
func (t *Thread) RetBool(b bool) {
|
|
t.retVal = MakeBool(b)
|
|
}
|
|
|
|
func (t *Thread) RetLong(n int64) {
|
|
t.retVal = MakeLong(n)
|
|
}
|
|
|
|
func (t *Thread) RetDouble(v float64, length, decimal uint16) {
|
|
t.retVal = MakeDouble(v, length, decimal)
|
|
}
|
|
|
|
func (t *Thread) RetPointer(val interface{}) {
|
|
t.retVal = MakePointer(val)
|
|
}
|
|
|
|
func (t *Thread) RetVal(v Value) {
|
|
t.retVal = v
|
|
}
|
|
|
|
// PushRetValue pushes the return value from the last call onto the stack.
|
|
func (t *Thread) PushRetValue() {
|
|
t.push(t.retVal)
|
|
}
|
|
|
|
// GetRetValue returns the current return value.
|
|
func (t *Thread) GetRetValue() Value {
|
|
return t.retVal
|
|
}
|
|
|
|
// --- Error handling ---
|
|
|
|
// HbError represents a Harbour runtime error.
|
|
type HbError struct {
|
|
Description string
|
|
Operation string
|
|
Args []Value
|
|
SubSystem string
|
|
GenCode int
|
|
Stack []DebugStackFrame // snapshot at panic site (pre-unwind)
|
|
}
|
|
|
|
func (e *HbError) Error() string {
|
|
return fmt.Sprintf("Five runtime error: %s (op: %s)", e.Description, e.Operation)
|
|
}
|
|
|
|
func (t *Thread) runtimeError(msg string) *HbError {
|
|
return &HbError{
|
|
Description: msg,
|
|
SubSystem: "BASE",
|
|
Stack: t.DebugCallStack(),
|
|
}
|
|
}
|
|
|
|
func (t *Thread) argError(op string, args ...Value) *HbError {
|
|
return &HbError{
|
|
Description: "argument error",
|
|
Operation: op,
|
|
Args: args,
|
|
SubSystem: "BASE",
|
|
GenCode: 1,
|
|
Stack: t.DebugCallStack(),
|
|
}
|
|
}
|
|
|
|
func (t *Thread) handleSequenceError(err *HbError) {
|
|
// BEGIN SEQUENCE / RECOVER: store error for RECOVER USING
|
|
t.lastError = err
|
|
// The recover block is handled by the generated code's defer/recover pattern.
|
|
// EndProc catches the panic and this function stores the error value.
|
|
}
|
|
|
|
// VM returns the VM this thread belongs to.
|
|
func (t *Thread) VM() *VM {
|
|
return t.vm
|
|
}
|
|
|
|
// ParamCount returns the number of parameters passed to the current call.
|
|
// Used by RTL functions that call ParamCount() BEFORE Frame() — returns
|
|
// pendingParams set by Function(nArgs). This is the original behavior
|
|
// that all existing RTL functions depend on.
|
|
//
|
|
// For PRG-level PCount(), use CallerParamCount() instead (via PCount RTL).
|
|
func (t *Thread) ParamCount() int {
|
|
return t.pendingParams
|
|
}
|
|
|
|
// CallerParamCount returns the param count of the calling PRG function
|
|
// (one frame below the current). Used by PCount() RTL which needs the
|
|
// caller's count, not its own.
|
|
func (t *Thread) CallerParamCount() int {
|
|
if t.callSP >= 2 {
|
|
return t.calls[t.callSP-2].paramCount
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// CallerLocal returns the n-th parameter of the calling PRG function
|
|
// (1-based). Returns NIL if no caller frame exists or n is out of range.
|
|
// Pairs with CallerParamCount for implementing the PValue() RTL.
|
|
//
|
|
// Reads from the caller frame's actualArgs snapshot — works for both
|
|
// plain and variadic callers. The locals[] slots would only carry the
|
|
// first declared-param-count values, which fails for FUNCTION foo(...)
|
|
// where declared params == 0.
|
|
func (t *Thread) CallerLocal(n int) Value {
|
|
if t.callSP >= 2 {
|
|
caller := &t.calls[t.callSP-2]
|
|
if n >= 1 && n <= len(caller.actualArgs) {
|
|
return caller.actualArgs[n-1]
|
|
}
|
|
}
|
|
return MakeNil()
|
|
}
|
|
|
|
// PendingParams2 sets pending param count for direct block calls (AEval, ASort etc.)
|
|
func (t *Thread) PendingParams2(n int) {
|
|
t.pendingParams = n
|
|
}
|
|
|
|
func (t *Thread) pushPendingSym(sym *Symbol) {
|
|
// Fast path for depth=1 nesting — store in scalar slot without
|
|
// touching the slice. A nil sym (unresolved symbol, caught later
|
|
// in Function() with a descriptive error) must not use the fast
|
|
// path because `pendingSymFast == nil` already means "empty";
|
|
// falling back to the slice preserves distinguishability.
|
|
if sym != nil && t.pendingSymFast == nil && len(t.pendingSyms) == 0 {
|
|
t.pendingSymFast = sym
|
|
return
|
|
}
|
|
if t.pendingSymFast != nil {
|
|
t.pendingSyms = append(t.pendingSyms, t.pendingSymFast)
|
|
t.pendingSymFast = nil
|
|
}
|
|
t.pendingSyms = append(t.pendingSyms, sym)
|
|
}
|
|
|
|
func (t *Thread) popPendingSym() *Symbol {
|
|
if n := len(t.pendingSyms); n > 0 {
|
|
sym := t.pendingSyms[n-1]
|
|
t.pendingSyms = t.pendingSyms[:n-1]
|
|
return sym
|
|
}
|
|
if sym := t.pendingSymFast; sym != nil {
|
|
t.pendingSymFast = nil
|
|
return sym
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PushAliasField pushes a field value from a named alias workarea.
|
|
// Harbour: alias->field
|
|
func (t *Thread) PushAliasField(alias, field string) {
|
|
// Delegate to WorkAreaManager via WA interface
|
|
if t.WA != nil {
|
|
// Use reflection-free interface assertion
|
|
type aliasGetter interface {
|
|
GetAliasField(alias, field string) Value
|
|
}
|
|
if ag, ok := t.WA.(aliasGetter); ok {
|
|
t.push(ag.GetAliasField(alias, field))
|
|
return
|
|
}
|
|
}
|
|
t.push(MakeNil())
|
|
}
|
|
|
|
// PushDynAliasField pushes a field from dynamic alias: (expr)->field
|
|
func (t *Thread) PushDynAliasField(alias, field string) {
|
|
t.PushAliasField(alias, field)
|
|
}
|
|
|
|
// GetLastError returns the last error from BEGIN SEQUENCE.
|
|
func (t *Thread) GetLastError() *HbError {
|
|
return t.lastError
|
|
}
|
|
|
|
// --- STATIC variable access ---
|
|
|
|
func (t *Thread) RegisterStatics(module string, statics []Value) {
|
|
t.statics[module] = statics
|
|
}
|
|
|
|
func (t *Thread) PushStatic(module string, n int) {
|
|
statics := t.statics[module]
|
|
if n < 1 || n > len(statics) {
|
|
panic(t.runtimeError(fmt.Sprintf("static index out of range: %s[%d]", module, n)))
|
|
}
|
|
t.push(statics[n-1])
|
|
}
|
|
|
|
func (t *Thread) PopStatic(module string, n int) {
|
|
statics := t.statics[module]
|
|
if n < 1 || n > len(statics) {
|
|
panic(t.runtimeError(fmt.Sprintf("static index out of range: %s[%d]", module, n)))
|
|
}
|
|
statics[n-1] = t.pop()
|
|
}
|
|
|
|
// --- Workarea context switching for (alias)->(expr) ---
|
|
//
|
|
// The waSel interfaces below use CurrentNum() (uint16 area index), NOT
|
|
// Current() (which returns the Area interface on WorkAreaManager). An
|
|
// earlier version required `Current() uint16` which silently failed the
|
|
// type assertion on the real hbrdd.WorkAreaManager implementation —
|
|
// `alias->(expr)` expressions appeared to "work" on the first area but
|
|
// collapsed to no-op as soon as a sibling area was opened, because the
|
|
// switch/save/restore block was skipped entirely. See repro in
|
|
// /tmp/repro_xarea.prg.
|
|
|
|
func (t *Thread) WASaveAndSelect(areaNum int) {
|
|
type waSel interface {
|
|
SelectByNum(uint16)
|
|
CurrentNum() uint16
|
|
}
|
|
if wam, ok := t.WA.(waSel); ok {
|
|
t.waStack = append(t.waStack, wam.CurrentNum())
|
|
wam.SelectByNum(uint16(areaNum))
|
|
}
|
|
}
|
|
|
|
func (t *Thread) WASaveAndSelectAlias(alias string) {
|
|
type waSel interface {
|
|
SelectByAlias(string) bool
|
|
SelectByNum(uint16)
|
|
CurrentNum() uint16
|
|
}
|
|
if wam, ok := t.WA.(waSel); ok {
|
|
t.waStack = append(t.waStack, wam.CurrentNum())
|
|
if !wam.SelectByAlias(alias) {
|
|
// Alias not open: switch to the no-area sentinel so the
|
|
// inner expression's Current() returns nil and ops like
|
|
// DbCloseArea/FieldGet/RecCount short-circuit. Without
|
|
// this, the inner expression silently runs against the
|
|
// originally-selected WA — which led to `CLOSE bad_alias`
|
|
// closing the *current* area.
|
|
wam.SelectByNum(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Thread) WARestore() {
|
|
if n := len(t.waStack); n > 0 {
|
|
saved := t.waStack[n-1]
|
|
t.waStack = t.waStack[:n-1]
|
|
type waSel interface{ SelectByNum(uint16) }
|
|
if wam, ok := t.WA.(waSel); ok {
|
|
wam.SelectByNum(saved)
|
|
}
|
|
}
|
|
}
|