Two stacked optimizations land on the SqlScan hot path. Combined
effect on the 50k-row benchmark:
Before After vs raw
Numeric WHERE 10.2ms 7.8ms 1.15x
String WHERE 10.5ms 7.9ms 1.15x
No WHERE 9.2ms 10.0ms 1.45x
Raw RDD baseline 6.8ms 6.8ms 1.00x
WHERE-predicate paths are now within 15% of the raw Harbour-style
RDD scan loop. The no-WHERE path is unchanged (slight jitter from
the added devirt branch); FieldGet peephole doesn't apply there.
--- Optimization 1: PcOpFieldGet peephole ---
Adds a new pcode opcode `PcOpFieldGet <fieldIdx>` (0x46) that skips
the usual PushSymbol+Function+Frame+FieldGet-RTL+EndProc chain and
calls a direct field getter closure instead. genpc recognizes the
shape `FieldGet(<int-literal>)` during emitCall and emits the
specialized opcode automatically — no SQL-side API change.
Integration:
* hbrt.Thread.FastFieldGetter — hot-path closure set by scan loops.
Non-nil → pcode bypasses dispatch.
Nil → pcode resolves FIELDGET via
the RTL symbol table (correctness
fallback for any other callers).
* compiler/genpc/genpc.go — peephole in emitCall.
* hbrt/pcinterp.go — PcOpFieldGet handler.
This alone cut numeric WHERE from 10.2 → 7.9ms: eliminated roughly
one full Frame/EndProc + RTL dispatch per row × 50k rows.
--- Optimization 2: DBFArea devirtualization ---
SqlScan type-asserts the workarea to *dbf.DBFArea once and runs a
dedicated loop that calls GoTop/EOF/Skip/GetValue directly on the
concrete type. Go's compiler inlines these, skipping the interface
vtable per row. Non-DBF drivers still work via the generic Area
branch.
The FastFieldGetter closure also captures *DBFArea directly in the
DBF branch, so the WHERE predicate side of the hot loop is now
entirely devirtualized: no interface dispatch between the pcode
dispatch loop and the DBF record buffer.
Validation:
- FiveSql2 43/43
- Harbour compat 51/51
- go test ./... ALL PASS
Remaining gap to raw RDD on no-WHERE (~1.45x) is dominated by the
two-column row construction + ArraySlab + flat backing bookkeeping
that the raw loop doesn't do. Going below that requires changing
the SQL engine's result shape — out of scope here.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
693 lines
18 KiB
Go
693 lines
18 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
|
|
}
|
|
|
|
// CurFrame returns the current call frame (for closure capture).
|
|
func (t *Thread) CurFrame() *CallFrame { return t.curFrame }
|
|
|
|
// 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.
|
|
type Thread struct {
|
|
// 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)
|
|
// Stack needed for nested calls: Double(Add(3,4))
|
|
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
|
|
|
|
// 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()) }
|
|
|
|
// --- 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
|
|
// Handle case where fewer args were pushed than declared params
|
|
actual := t.pendingParams
|
|
if actual > params {
|
|
actual = params
|
|
}
|
|
if actual > t.sp {
|
|
actual = t.sp
|
|
}
|
|
|
|
frame := &t.calls[t.callSP]
|
|
frame.base = t.sp - actual // only actual args on stack
|
|
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
|
|
|
|
// Copy actual parameters from stack to locals
|
|
for i := 0; i < actual; i++ {
|
|
t.locals[localBase+i] = t.stack[frame.base+i]
|
|
}
|
|
|
|
// Initialize missing params and locals to NIL
|
|
for i := actual; i < params+locals; i++ {
|
|
t.locals[localBase+i] = MakeNil()
|
|
}
|
|
|
|
// Pop args from stack (they're now in locals)
|
|
t.sp = frame.base
|
|
|
|
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
|
|
}
|
|
|
|
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",
|
|
}
|
|
}
|
|
|
|
func (t *Thread) argError(op string, args ...Value) *HbError {
|
|
return &HbError{
|
|
Description: "argument error",
|
|
Operation: op,
|
|
Args: args,
|
|
SubSystem: "BASE",
|
|
GenCode: 1,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
t.pendingSyms = append(t.pendingSyms, sym)
|
|
}
|
|
|
|
func (t *Thread) popPendingSym() *Symbol {
|
|
n := len(t.pendingSyms)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
sym := t.pendingSyms[n-1]
|
|
t.pendingSyms = t.pendingSyms[:n-1]
|
|
return sym
|
|
}
|
|
|
|
// 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) ---
|
|
|
|
func (t *Thread) WASaveAndSelect(areaNum int) {
|
|
type waSel interface{ SelectByNum(uint16); Current() uint16 }
|
|
if wam, ok := t.WA.(waSel); ok {
|
|
t.waStack = append(t.waStack, wam.Current())
|
|
wam.SelectByNum(uint16(areaNum))
|
|
}
|
|
}
|
|
|
|
func (t *Thread) WASaveAndSelectAlias(alias string) {
|
|
type waSel interface{ SelectByAlias(string); Current() uint16 }
|
|
if wam, ok := t.WA.(waSel); ok {
|
|
t.waStack = append(t.waStack, wam.Current())
|
|
wam.SelectByAlias(alias)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|