Files
five/hbrt/thread.go
Charles KWON OhJun 05ccef05e2 perf: EndProcFast — eliminate defer recover() from RTL hot paths
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>
2026-04-07 21:43:39 +09:00

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()
}