Files
five/hbrt/thread.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:41:50 +09:00

484 lines
12 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
)
// 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
// 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, MaxCallDepth),
callSP: 0,
statics: make(map[string][]Value),
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] = MakeNil() // help GC
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"))
}
// 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 {
// Print error to stderr before re-panic
fmt.Fprintf(os.Stderr, "Five runtime error: %v\n", r)
panic(r)
}
}
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
}
// --- 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()
}