Problem: every RTL function calls defer t.EndProc() which does recover(). 50K SEEK loop = 250K recover() calls = ~12ms wasted. Solution: EndProcFast() skips recover (only needs endFrame restore). Applied to ALL RTL functions in strings.go, rdd.go, missing.go, database.go. EndProc() with recover kept for generated PRG code (needs BEGIN SEQUENCE). Analysis (50K sequential SEEK breakdown): Go NTX Seek direct: 7ms (faster than Harbour 27ms!) PRG VM overhead: 38ms (Frame + RTL calls + key generation) Key generation: 25ms (Str+LTrim+PadL+PadR = 5 RTL Frame/EndProc per iter) With EndProcFast: RTL overhead reduced ~30%. CDX SCOPE: 2ms (Harbour 4ms — 2x FASTER!) 82/82 stress PASS. 14 packages ALL PASS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
537 lines
13 KiB
Go
537 lines
13 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
|
|
|
|
// 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 {
|
|
t.sp--
|
|
v := t.stack[t.sp]
|
|
t.stack[t.sp] = cachedNil // help GC (no alloc)
|
|
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 = params
|
|
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.
|
|
func (t *Thread) EndProc() {
|
|
if r := recover(); r != nil {
|
|
if hbErr, ok := r.(*HbError); ok {
|
|
t.handleSequenceError(hbErr)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Five runtime error: %v\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)
|
|
t.push(t.locals[idx])
|
|
}
|
|
|
|
func (t *Thread) PopLocal(n int) {
|
|
idx := t.localIndex(n)
|
|
t.locals[idx] = t.pop()
|
|
}
|
|
|
|
func (t *Thread) Local(n int) Value {
|
|
return t.locals[t.localIndex(n)]
|
|
}
|
|
|
|
func (t *Thread) SetLocal(n int, v Value) {
|
|
t.locals[t.localIndex(n)] = v
|
|
}
|
|
|
|
// PushLocalRef pushes a reference to a local variable (for @param).
|
|
// Harbour: hb_vmPushLocalByRef
|
|
// Simplified: pushes the value (true BYREF needs refcell pattern).
|
|
// TODO: implement proper ByRef with shared mutation.
|
|
func (t *Thread) PushLocalRef(n int) {
|
|
t.push(t.Local(n)) // simplified: pass by value for now
|
|
}
|
|
|
|
func (t *Thread) LocalAsString(n int) string {
|
|
return t.Local(n).AsString()
|
|
}
|
|
|
|
// LocalSetInt is an optimization: set local directly without stack.
|
|
// Harbour: hb_xvmLocalSetInt(n, val)
|
|
func (t *Thread) LocalSetInt(n int, val int) {
|
|
t.locals[t.localIndex(n)] = 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 variadic RTL functions (QOut, etc.).
|
|
func (t *Thread) ParamCount() int {
|
|
return t.pendingParams
|
|
}
|
|
|
|
// 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()
|
|
}
|