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>
This commit is contained in:
72
hbrt/call.go
Normal file
72
hbrt/call.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import "strings"
|
||||
|
||||
// pendingCall stores the symbol for the next Function/Do call.
|
||||
// This avoids storing Go pointers in Value.data (which GC can't trace).
|
||||
|
||||
// PushSymbol records the function symbol for the next call.
|
||||
// The actual symbol is stored in Thread, not on the eval stack.
|
||||
// A marker NIL is pushed to keep stack positions correct.
|
||||
// Harbour: hb_xvmPushSymbol
|
||||
func (t *Thread) PushSymbol(sym *Symbol) {
|
||||
t.pushPendingSym(sym)
|
||||
t.push(MakeNil()) // placeholder for symbol position
|
||||
}
|
||||
|
||||
// Function calls the function with nArgs arguments.
|
||||
// Stack layout before: [sym_placeholder] [nil/self] [arg1] ... [argN]
|
||||
// Stack after: [retval]
|
||||
// Harbour: hb_xvmFunction
|
||||
func (t *Thread) Function(nArgs int) {
|
||||
sym := t.popPendingSym()
|
||||
|
||||
if sym == nil {
|
||||
panic(t.runtimeError("no function symbol for call"))
|
||||
}
|
||||
|
||||
// Resolve function
|
||||
fn := sym.Func
|
||||
if fn == nil && t.vm != nil {
|
||||
found := t.vm.FindSymbol(strings.ToUpper(sym.Name))
|
||||
if found != nil {
|
||||
fn = found.Func
|
||||
}
|
||||
}
|
||||
if fn == nil {
|
||||
panic(t.runtimeError("undefined function: " + sym.Name))
|
||||
}
|
||||
|
||||
// Collect args from stack
|
||||
args := make([]Value, nArgs)
|
||||
for i := nArgs - 1; i >= 0; i-- {
|
||||
args[i] = t.pop()
|
||||
}
|
||||
t.pop() // pop NIL/self placeholder
|
||||
t.pop() // pop symbol placeholder
|
||||
|
||||
// Push args back for Frame() to pick up
|
||||
for _, arg := range args {
|
||||
t.push(arg)
|
||||
}
|
||||
|
||||
// Set pending params count and symbol for Frame()
|
||||
t.pendingParams = nArgs
|
||||
t.pendingCallSym = sym
|
||||
|
||||
// Call
|
||||
fn(t)
|
||||
|
||||
// Push return value
|
||||
t.push(t.retVal)
|
||||
}
|
||||
|
||||
// Do calls the function but discards the return value.
|
||||
// Harbour: hb_xvmDo
|
||||
func (t *Thread) Do(nArgs int) {
|
||||
t.Function(nArgs)
|
||||
t.pop() // discard return value
|
||||
}
|
||||
345
hbrt/class.go
Normal file
345
hbrt/class.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// CLASS system for the Five runtime.
|
||||
// Harbour: classes.c — object = array with uiClass, methods in class registry.
|
||||
//
|
||||
// Key concepts:
|
||||
// - Class = definition (name, DATA fields, METHODs, parent)
|
||||
// - Object = HbArray with Class > 0 (fields stored in Items[])
|
||||
// - Send = method dispatch: obj:method(args) → lookup class → call func
|
||||
// - :: = Self access (current object in method context)
|
||||
// - INHERIT FROM = parent class embedding
|
||||
// - Operator overloading: HB_OO_OP_PLUS etc.
|
||||
//
|
||||
// Reference:
|
||||
// /mnt/d/harbour-core/include/hbapicls.h
|
||||
// /mnt/d/harbour-core/src/vm/classes.c
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ClassDef defines a class (DATA fields + METHODs).
|
||||
// Harbour: internal class structure in classes.c
|
||||
type ClassDef struct {
|
||||
ID uint16
|
||||
Name string
|
||||
Parent *ClassDef // INHERIT FROM
|
||||
Fields []ClassField // DATA declarations (ordered)
|
||||
Methods map[string]MethodFunc // method name → function
|
||||
Operators [MaxOperator + 1]MethodFunc // operator overloading
|
||||
fieldMap map[string]int // field name → index
|
||||
}
|
||||
|
||||
// ClassField describes a DATA member.
|
||||
type ClassField struct {
|
||||
Name string
|
||||
Init Value // default INIT value
|
||||
AsType string // optional type hint
|
||||
}
|
||||
|
||||
// MethodFunc is the signature for class methods.
|
||||
// The method receives the thread; Self is available via thread context.
|
||||
type MethodFunc func(t *Thread)
|
||||
|
||||
// Operator IDs matching Harbour's HB_OO_OP_*
|
||||
const (
|
||||
OpPlus = 0
|
||||
OpMinus = 1
|
||||
OpMult = 2
|
||||
OpDivide = 3
|
||||
OpMod = 4
|
||||
OpPower = 5
|
||||
OpInc = 6
|
||||
OpDec = 7
|
||||
OpEqual = 8
|
||||
OpExactEqual = 9
|
||||
OpNotEqual = 10
|
||||
OpLess = 11
|
||||
OpLessEqual = 12
|
||||
OpGreater = 13
|
||||
OpGreaterEqual = 14
|
||||
OpAssign = 15
|
||||
OpInString = 16
|
||||
OpInclude = 17
|
||||
OpNot = 18
|
||||
OpAnd = 19
|
||||
OpOr = 20
|
||||
OpArrayIndex = 21
|
||||
MaxOperator = 21
|
||||
)
|
||||
|
||||
// --- Class Registry ---
|
||||
|
||||
var (
|
||||
classRegMu sync.RWMutex
|
||||
classReg = map[string]*ClassDef{}
|
||||
classList []*ClassDef // index = classID - 1
|
||||
)
|
||||
|
||||
// RegisterClass registers a class definition.
|
||||
// Returns the assigned class ID (1-based).
|
||||
func RegisterClass(cls *ClassDef) uint16 {
|
||||
classRegMu.Lock()
|
||||
defer classRegMu.Unlock()
|
||||
|
||||
cls.ID = uint16(len(classList) + 1)
|
||||
classList = append(classList, cls)
|
||||
classReg[strings.ToUpper(cls.Name)] = cls
|
||||
|
||||
// Build field index map
|
||||
cls.fieldMap = make(map[string]int, len(cls.Fields))
|
||||
for i, f := range cls.Fields {
|
||||
cls.fieldMap[strings.ToUpper(f.Name)] = i
|
||||
}
|
||||
|
||||
return cls.ID
|
||||
}
|
||||
|
||||
// FindClass looks up a class by name.
|
||||
func FindClass(name string) *ClassDef {
|
||||
classRegMu.RLock()
|
||||
defer classRegMu.RUnlock()
|
||||
return classReg[strings.ToUpper(name)]
|
||||
}
|
||||
|
||||
// GetClass looks up a class by ID.
|
||||
func GetClass(id uint16) *ClassDef {
|
||||
classRegMu.RLock()
|
||||
defer classRegMu.RUnlock()
|
||||
if int(id) > 0 && int(id) <= len(classList) {
|
||||
return classList[id-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Class builder (fluent API for generated code) ---
|
||||
|
||||
// NewClassDef creates a new class definition builder.
|
||||
func NewClassDef(name string) *ClassDef {
|
||||
return &ClassDef{
|
||||
Name: name,
|
||||
Methods: make(map[string]MethodFunc),
|
||||
}
|
||||
}
|
||||
|
||||
// InheritFrom sets the parent class.
|
||||
func (c *ClassDef) InheritFrom(parentName string) *ClassDef {
|
||||
parent := FindClass(parentName)
|
||||
if parent != nil {
|
||||
c.Parent = parent
|
||||
// Copy parent fields first
|
||||
c.Fields = append(append([]ClassField{}, parent.Fields...), c.Fields...)
|
||||
// Copy parent methods (child can override)
|
||||
for name, fn := range parent.Methods {
|
||||
if _, exists := c.Methods[name]; !exists {
|
||||
c.Methods[name] = fn
|
||||
}
|
||||
}
|
||||
// Copy parent operators
|
||||
for i, fn := range parent.Operators {
|
||||
if c.Operators[i] == nil {
|
||||
c.Operators[i] = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AddData adds a DATA field to the class.
|
||||
func (c *ClassDef) AddData(name string, init Value) *ClassDef {
|
||||
c.Fields = append(c.Fields, ClassField{Name: name, Init: init})
|
||||
return c
|
||||
}
|
||||
|
||||
// AddMethod adds a METHOD to the class.
|
||||
func (c *ClassDef) AddMethod(name string, fn MethodFunc) *ClassDef {
|
||||
c.Methods[strings.ToUpper(name)] = fn
|
||||
return c
|
||||
}
|
||||
|
||||
// AddOperator sets an operator overload.
|
||||
func (c *ClassDef) AddOperator(op int, fn MethodFunc) *ClassDef {
|
||||
if op >= 0 && op <= MaxOperator {
|
||||
c.Operators[op] = fn
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Register registers this class and returns the class ID.
|
||||
func (c *ClassDef) Register() uint16 {
|
||||
return RegisterClass(c)
|
||||
}
|
||||
|
||||
// FieldIndex returns the 0-based field index by name, or -1.
|
||||
func (c *ClassDef) FieldIndex(name string) int {
|
||||
if c.fieldMap == nil {
|
||||
return -1
|
||||
}
|
||||
if idx, ok := c.fieldMap[strings.ToUpper(name)]; ok {
|
||||
return idx
|
||||
}
|
||||
// Check parent
|
||||
if c.Parent != nil {
|
||||
return c.Parent.FieldIndex(name)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// --- Object creation ---
|
||||
|
||||
// NewObject creates a new object instance of a class.
|
||||
// Harbour: object = array with uiClass set.
|
||||
func NewObject(classID uint16) Value {
|
||||
cls := GetClass(classID)
|
||||
if cls == nil {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
obj := MakeObject(classID, len(cls.Fields))
|
||||
arr := obj.AsArray()
|
||||
|
||||
// Initialize fields with default values
|
||||
for i, f := range cls.Fields {
|
||||
arr.Items[i] = f.Init
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// --- Method dispatch ---
|
||||
|
||||
// Send dispatches a method call on an object.
|
||||
// Harbour: hb_objGetMethod + call
|
||||
// Stack: [object] [arg1] ... [argN] → call method → [result]
|
||||
func (t *Thread) Send(methodName string, nArgs int) {
|
||||
// Collect args
|
||||
args := make([]Value, nArgs)
|
||||
for i := nArgs - 1; i >= 0; i-- {
|
||||
args[i] = t.pop()
|
||||
}
|
||||
objVal := t.pop() // object
|
||||
|
||||
if !objVal.IsObject() {
|
||||
// Not an object — try as property access on non-object
|
||||
panic(t.runtimeError(fmt.Sprintf("not an object for method %s", methodName)))
|
||||
}
|
||||
|
||||
arr := objVal.AsArray()
|
||||
cls := GetClass(arr.Class)
|
||||
if cls == nil {
|
||||
panic(t.runtimeError(fmt.Sprintf("unknown class ID %d", arr.Class)))
|
||||
}
|
||||
|
||||
upperMethod := strings.ToUpper(methodName)
|
||||
|
||||
// Check for data field access (getter)
|
||||
if nArgs == 0 {
|
||||
if idx := cls.FieldIndex(methodName); idx >= 0 {
|
||||
t.push(arr.Items[idx])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for data field setter: _FIELDNAME convention
|
||||
if nArgs == 1 && strings.HasPrefix(upperMethod, "_") {
|
||||
fieldName := upperMethod[1:]
|
||||
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
||||
arr.Items[idx] = args[0]
|
||||
t.push(args[0])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Look up method
|
||||
fn, ok := cls.Methods[upperMethod]
|
||||
if !ok {
|
||||
panic(t.runtimeError(fmt.Sprintf("unknown method %s in class %s", methodName, cls.Name)))
|
||||
}
|
||||
|
||||
// Set up Self context
|
||||
oldSelf := t.self
|
||||
t.self = objVal
|
||||
|
||||
// Push args for Frame
|
||||
for _, arg := range args {
|
||||
t.push(arg)
|
||||
}
|
||||
|
||||
t.pendingParams = nArgs
|
||||
fn(t)
|
||||
|
||||
// Restore Self
|
||||
t.self = oldSelf
|
||||
|
||||
// Push return value
|
||||
t.push(t.retVal)
|
||||
}
|
||||
|
||||
// SendAssign dispatches a setter: obj:prop := value
|
||||
// Generated for ::fieldName := value
|
||||
func (t *Thread) SendAssign(fieldName string) {
|
||||
val := t.pop()
|
||||
objVal := t.pop()
|
||||
|
||||
if !objVal.IsObject() {
|
||||
panic(t.runtimeError("not an object for assignment"))
|
||||
}
|
||||
|
||||
arr := objVal.AsArray()
|
||||
cls := GetClass(arr.Class)
|
||||
if cls == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
||||
arr.Items[idx] = val
|
||||
}
|
||||
}
|
||||
|
||||
// Send0 dispatches a no-arg method (getter).
|
||||
func (t *Thread) Send0(methodName string) {
|
||||
t.Send(methodName, 0)
|
||||
}
|
||||
|
||||
// PushSelfField pushes a field from the current Self object.
|
||||
// Used by :: access in methods.
|
||||
func (t *Thread) PushSelfField(fieldName string) {
|
||||
if t.self.IsNil() {
|
||||
t.push(MakeNil())
|
||||
return
|
||||
}
|
||||
arr := t.self.AsArray()
|
||||
cls := GetClass(arr.Class)
|
||||
if cls != nil {
|
||||
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
||||
t.push(arr.Items[idx])
|
||||
return
|
||||
}
|
||||
}
|
||||
t.push(MakeNil())
|
||||
}
|
||||
|
||||
// SetSelfField sets a field on the current Self object.
|
||||
func (t *Thread) SetSelfField(fieldName string) {
|
||||
val := t.pop()
|
||||
if t.self.IsNil() {
|
||||
return
|
||||
}
|
||||
arr := t.self.AsArray()
|
||||
cls := GetClass(arr.Class)
|
||||
if cls != nil {
|
||||
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
||||
arr.Items[idx] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelf returns the current Self value.
|
||||
func (t *Thread) GetSelf() Value {
|
||||
return t.self
|
||||
}
|
||||
215
hbrt/class_test.go
Normal file
215
hbrt/class_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClassCreateAndInstantiate(t *testing.T) {
|
||||
cls := NewClassDef("Person").
|
||||
AddData("CNAME", MakeString("")).
|
||||
AddData("NAGE", MakeInt(0)).
|
||||
Register()
|
||||
|
||||
obj := NewObject(cls)
|
||||
if !obj.IsObject() {
|
||||
t.Fatal("should be object")
|
||||
}
|
||||
|
||||
arr := obj.AsArray()
|
||||
if arr.Class != cls {
|
||||
t.Errorf("class = %d, want %d", arr.Class, cls)
|
||||
}
|
||||
if len(arr.Items) != 2 {
|
||||
t.Fatalf("fields = %d, want 2", len(arr.Items))
|
||||
}
|
||||
|
||||
// Default values
|
||||
if arr.Items[0].AsString() != "" {
|
||||
t.Errorf("cName default = %q, want empty", arr.Items[0].AsString())
|
||||
}
|
||||
if arr.Items[1].AsInt() != 0 {
|
||||
t.Errorf("nAge default = %d, want 0", arr.Items[1].AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassMethodDispatch(t *testing.T) {
|
||||
cls := NewClassDef("Person")
|
||||
cls.AddData("CNAME", MakeString(""))
|
||||
cls.AddData("NAGE", MakeInt(0))
|
||||
|
||||
// METHOD New(cName, nAge)
|
||||
cls.AddMethod("NEW", func(th *Thread) {
|
||||
th.Frame(2, 0)
|
||||
defer th.EndProc()
|
||||
// ::cName := param1
|
||||
th.PushValue(th.Local(1))
|
||||
th.SetSelfField("CNAME")
|
||||
// ::nAge := param2
|
||||
th.PushValue(th.Local(2))
|
||||
th.SetSelfField("NAGE")
|
||||
// RETURN Self
|
||||
th.PushSelf()
|
||||
th.RetValue()
|
||||
})
|
||||
|
||||
// METHOD Greet() → "Hello, I'm " + ::cName
|
||||
cls.AddMethod("GREET", func(th *Thread) {
|
||||
th.Frame(0, 0)
|
||||
defer th.EndProc()
|
||||
th.PushString("Hello, I'm ")
|
||||
th.PushSelfField("CNAME")
|
||||
th.Plus()
|
||||
th.RetValue()
|
||||
})
|
||||
|
||||
cls.Register()
|
||||
|
||||
// Create instance
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
// obj := Person():New("Kim", 30)
|
||||
clsID := FindClass("Person").ID
|
||||
obj := NewObject(clsID)
|
||||
th.push(obj)
|
||||
th.PushString("Kim")
|
||||
th.PushInt(30)
|
||||
th.Send("NEW", 2)
|
||||
resultObj := th.pop()
|
||||
|
||||
// Verify fields were set
|
||||
arr := resultObj.AsArray()
|
||||
if arr.Items[0].AsString() != "Kim" {
|
||||
t.Errorf("cName = %q, want Kim", arr.Items[0].AsString())
|
||||
}
|
||||
if arr.Items[1].AsInt() != 30 {
|
||||
t.Errorf("nAge = %d, want 30", arr.Items[1].AsInt())
|
||||
}
|
||||
|
||||
// Call Greet
|
||||
th.push(resultObj)
|
||||
th.Send("GREET", 0)
|
||||
greeting := th.pop()
|
||||
if greeting.AsString() != "Hello, I'm Kim" {
|
||||
t.Errorf("greet = %q, want %q", greeting.AsString(), "Hello, I'm Kim")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestClassFieldGetterSetter(t *testing.T) {
|
||||
cls := NewClassDef("Point")
|
||||
cls.AddData("X", MakeInt(0))
|
||||
cls.AddData("Y", MakeInt(0))
|
||||
cls.Register()
|
||||
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
obj := NewObject(FindClass("Point").ID)
|
||||
|
||||
// Getter: obj:X
|
||||
th.push(obj)
|
||||
th.Send("X", 0)
|
||||
if th.pop().AsInt() != 0 {
|
||||
t.Error("X default should be 0")
|
||||
}
|
||||
|
||||
// Setter: obj:_X := 10 (convention: _FIELDNAME for setter)
|
||||
th.push(obj)
|
||||
th.PushInt(10)
|
||||
th.Send("_X", 1)
|
||||
th.pop() // discard setter result
|
||||
|
||||
// Verify
|
||||
th.push(obj)
|
||||
th.Send("X", 0)
|
||||
if th.pop().AsInt() != 10 {
|
||||
t.Error("X should be 10 after setter")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestClassInheritance(t *testing.T) {
|
||||
// Parent: Animal
|
||||
animal := NewClassDef("Animal")
|
||||
animal.AddData("CNAME", MakeString(""))
|
||||
animal.AddMethod("SPEAK", func(th *Thread) {
|
||||
th.Frame(0, 0)
|
||||
defer th.EndProc()
|
||||
th.PushString("...")
|
||||
th.RetValue()
|
||||
})
|
||||
animal.Register()
|
||||
|
||||
// Child: Dog INHERIT FROM Animal
|
||||
dog := NewClassDef("Dog")
|
||||
dog.InheritFrom("Animal")
|
||||
// Override SPEAK
|
||||
dog.AddMethod("SPEAK", func(th *Thread) {
|
||||
th.Frame(0, 0)
|
||||
defer th.EndProc()
|
||||
th.PushString("Woof!")
|
||||
th.RetValue()
|
||||
})
|
||||
// Add new method
|
||||
dog.AddMethod("FETCH", func(th *Thread) {
|
||||
th.Frame(0, 0)
|
||||
defer th.EndProc()
|
||||
th.PushSelfField("CNAME")
|
||||
th.PushString(" fetches the ball!")
|
||||
th.Plus()
|
||||
th.RetValue()
|
||||
})
|
||||
dog.Register()
|
||||
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
// Create Dog
|
||||
obj := NewObject(FindClass("Dog").ID)
|
||||
arr := obj.AsArray()
|
||||
arr.Items[0] = MakeString("Rex") // set cName
|
||||
|
||||
// Dog:Speak → "Woof!" (overridden)
|
||||
th.push(obj)
|
||||
th.Send("SPEAK", 0)
|
||||
if th.pop().AsString() != "Woof!" {
|
||||
t.Error("Dog:Speak should be 'Woof!'")
|
||||
}
|
||||
|
||||
// Dog:Fetch → "Rex fetches the ball!" (new method using inherited field)
|
||||
th.push(obj)
|
||||
th.Send("FETCH", 0)
|
||||
result := th.pop().AsString()
|
||||
if result != "Rex fetches the ball!" {
|
||||
t.Errorf("Dog:Fetch = %q", result)
|
||||
}
|
||||
|
||||
// Dog has inherited cName from Animal
|
||||
th.push(obj)
|
||||
th.Send("CNAME", 0)
|
||||
if th.pop().AsString() != "Rex" {
|
||||
t.Error("inherited CNAME should be Rex")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestClassFindAndRegistry(t *testing.T) {
|
||||
// These classes were registered in previous tests
|
||||
if FindClass("Person") == nil {
|
||||
t.Error("Person class should be registered")
|
||||
}
|
||||
if FindClass("PERSON") == nil {
|
||||
t.Error("case-insensitive lookup should work")
|
||||
}
|
||||
if FindClass("NonExistent") != nil {
|
||||
t.Error("non-existent class should return nil")
|
||||
}
|
||||
}
|
||||
309
hbrt/debug.go
Normal file
309
hbrt/debug.go
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Five Debugger — line-level debugging with breakpoints, stepping,
|
||||
// variable inspection, and call stack display.
|
||||
//
|
||||
// Architecture:
|
||||
// gengo emits t.DebugLine(file, line) at each PRG source line.
|
||||
// Thread.DebugLine() checks breakpoints and invokes the debugger callback.
|
||||
// The debugger callback can inspect variables, call stack, and control flow.
|
||||
//
|
||||
// Unlike Harbour's 370KB debug system, Five's debugger is ~300 lines of Go
|
||||
// that leverages Go's runtime introspection.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DebugMode constants
|
||||
const (
|
||||
DbgContinue = 0 // free run, stop at breakpoints only
|
||||
DbgStepLine = 1 // stop at every line
|
||||
DbgStepOver = 2 // stop when returning to same or shallower level
|
||||
DbgStepOut = 3 // stop when returning from current function
|
||||
DbgToCursor = 4 // run until specific line
|
||||
)
|
||||
|
||||
// Breakpoint represents a source-level breakpoint.
|
||||
type Breakpoint struct {
|
||||
Module string // source file name
|
||||
Line int // line number (1-based)
|
||||
Function string // optional function name filter
|
||||
Enabled bool
|
||||
HitCount int
|
||||
}
|
||||
|
||||
// DebugVarInfo describes a variable visible in the current scope.
|
||||
type DebugVarInfo struct {
|
||||
Name string // variable name
|
||||
Value Value // current value
|
||||
Scope string // "LOCAL", "STATIC", "PARAM"
|
||||
Index int // local index (1-based)
|
||||
}
|
||||
|
||||
// DebugStackFrame describes one frame in the call stack.
|
||||
type DebugStackFrame struct {
|
||||
Function string // function name
|
||||
Module string // source file
|
||||
Line int // current line
|
||||
Level int // stack depth
|
||||
}
|
||||
|
||||
// DebugCallback is called when the debugger activates.
|
||||
// The callback can inspect state and return the next debug mode.
|
||||
type DebugCallback func(info *DebugEvent) int
|
||||
|
||||
// DebugEvent contains all information available at a debug stop.
|
||||
type DebugEvent struct {
|
||||
Module string // current source file
|
||||
Line int // current line number
|
||||
Function string // current function name
|
||||
Reason string // "breakpoint", "step", "entry"
|
||||
Thread *Thread // the executing thread
|
||||
CallStack []DebugStackFrame // call stack
|
||||
Locals []DebugVarInfo // local variables
|
||||
Breakpoint *Breakpoint // which breakpoint hit (or nil)
|
||||
}
|
||||
|
||||
// Debugger manages debug state for the VM.
|
||||
type Debugger struct {
|
||||
mu sync.Mutex
|
||||
Enabled bool
|
||||
Mode int // DbgContinue, DbgStepLine, etc.
|
||||
Breakpoints []*Breakpoint
|
||||
Callback DebugCallback // called when debugger activates
|
||||
StepLevel int // call stack depth for step-over
|
||||
ToCursorMod string // target module for run-to-cursor
|
||||
ToCursorLine int // target line for run-to-cursor
|
||||
|
||||
// Debug info tables (populated by generated code)
|
||||
LineInfo map[string]map[int]bool // module → set of valid lines
|
||||
FuncInfo map[string]string // "MODULE:LINE" → function name
|
||||
LocalInfo map[string][]string // function → local var names
|
||||
SourceDir string // base directory for resolving source paths
|
||||
}
|
||||
|
||||
// NewDebugger creates a new debugger instance.
|
||||
func NewDebugger() *Debugger {
|
||||
return &Debugger{
|
||||
Enabled: true,
|
||||
Mode: DbgStepLine, // start in step mode
|
||||
LineInfo: make(map[string]map[int]bool),
|
||||
FuncInfo: make(map[string]string),
|
||||
LocalInfo: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
// AddBreakpoint adds a breakpoint.
|
||||
func (d *Debugger) AddBreakpoint(module string, line int) int {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
bp := &Breakpoint{
|
||||
Module: strings.ToUpper(module),
|
||||
Line: line,
|
||||
Enabled: true,
|
||||
}
|
||||
d.Breakpoints = append(d.Breakpoints, bp)
|
||||
return len(d.Breakpoints) - 1
|
||||
}
|
||||
|
||||
// RemoveBreakpoint removes a breakpoint by index.
|
||||
func (d *Debugger) RemoveBreakpoint(idx int) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if idx >= 0 && idx < len(d.Breakpoints) {
|
||||
d.Breakpoints = append(d.Breakpoints[:idx], d.Breakpoints[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleBreakpoint enables/disables a breakpoint.
|
||||
func (d *Debugger) ToggleBreakpoint(idx int) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if idx >= 0 && idx < len(d.Breakpoints) {
|
||||
d.Breakpoints[idx].Enabled = !d.Breakpoints[idx].Enabled
|
||||
}
|
||||
}
|
||||
|
||||
// FindBreakpoint checks if there's an active breakpoint at module:line.
|
||||
func (d *Debugger) FindBreakpoint(module string, line int) *Breakpoint {
|
||||
upper := strings.ToUpper(module)
|
||||
for _, bp := range d.Breakpoints {
|
||||
if bp.Enabled && bp.Line == line {
|
||||
if bp.Module == upper || strings.HasSuffix(upper, bp.Module) {
|
||||
bp.HitCount++
|
||||
return bp
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterLine records that a line exists in a module (for valid breakpoint checking).
|
||||
func (d *Debugger) RegisterLine(module string, line int) {
|
||||
upper := strings.ToUpper(module)
|
||||
if d.LineInfo[upper] == nil {
|
||||
d.LineInfo[upper] = make(map[int]bool)
|
||||
}
|
||||
d.LineInfo[upper][line] = true
|
||||
}
|
||||
|
||||
// IsValidLine checks if a line is a valid stop point.
|
||||
func (d *Debugger) IsValidLine(module string, line int) bool {
|
||||
upper := strings.ToUpper(module)
|
||||
if lines, ok := d.LineInfo[upper]; ok {
|
||||
return lines[line]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- Thread debug integration ---
|
||||
|
||||
// DebugLine is called by generated code at each PRG source line.
|
||||
// This is the main debug hook — gengo emits t.DebugLine("file.prg", 42)
|
||||
func (t *Thread) DebugLine(module string, line int) {
|
||||
vm := t.VM()
|
||||
if vm.Debugger == nil || !vm.Debugger.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
dbg := vm.Debugger
|
||||
dbg.mu.Lock()
|
||||
mode := dbg.Mode
|
||||
dbg.mu.Unlock()
|
||||
|
||||
shouldStop := false
|
||||
var hitBP *Breakpoint
|
||||
reason := ""
|
||||
|
||||
switch mode {
|
||||
case DbgContinue:
|
||||
// Only stop at breakpoints
|
||||
hitBP = dbg.FindBreakpoint(module, line)
|
||||
if hitBP != nil {
|
||||
shouldStop = true
|
||||
reason = "breakpoint"
|
||||
}
|
||||
case DbgStepLine:
|
||||
shouldStop = true
|
||||
reason = "step"
|
||||
case DbgStepOver:
|
||||
if t.callSP <= dbg.StepLevel {
|
||||
shouldStop = true
|
||||
reason = "step"
|
||||
} else {
|
||||
// Check breakpoints even during step-over
|
||||
hitBP = dbg.FindBreakpoint(module, line)
|
||||
if hitBP != nil {
|
||||
shouldStop = true
|
||||
reason = "breakpoint"
|
||||
}
|
||||
}
|
||||
case DbgStepOut:
|
||||
if t.callSP < dbg.StepLevel {
|
||||
shouldStop = true
|
||||
reason = "step"
|
||||
}
|
||||
case DbgToCursor:
|
||||
if strings.EqualFold(module, dbg.ToCursorMod) && line == dbg.ToCursorLine {
|
||||
shouldStop = true
|
||||
reason = "cursor"
|
||||
} else {
|
||||
hitBP = dbg.FindBreakpoint(module, line)
|
||||
if hitBP != nil {
|
||||
shouldStop = true
|
||||
reason = "breakpoint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldStop {
|
||||
return
|
||||
}
|
||||
|
||||
// Build debug event
|
||||
event := &DebugEvent{
|
||||
Module: module,
|
||||
Line: line,
|
||||
Function: t.currentFuncName(),
|
||||
Reason: reason,
|
||||
Thread: t,
|
||||
Breakpoint: hitBP,
|
||||
}
|
||||
|
||||
// Build call stack
|
||||
event.CallStack = t.DebugCallStack()
|
||||
|
||||
// Build locals
|
||||
event.Locals = t.DebugLocals()
|
||||
|
||||
// Invoke callback
|
||||
if dbg.Callback != nil {
|
||||
newMode := dbg.Callback(event)
|
||||
dbg.mu.Lock()
|
||||
dbg.Mode = newMode
|
||||
if newMode == DbgStepOver {
|
||||
dbg.StepLevel = t.callSP
|
||||
} else if newMode == DbgStepOut {
|
||||
dbg.StepLevel = t.callSP
|
||||
}
|
||||
dbg.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// DebugCallStack returns the current call stack for debugging.
|
||||
func (t *Thread) DebugCallStack() []DebugStackFrame {
|
||||
var stack []DebugStackFrame
|
||||
for i := t.callSP - 1; i >= 0; i-- {
|
||||
frame := &t.calls[i]
|
||||
name := "unknown"
|
||||
if frame.symbol != nil {
|
||||
name = frame.symbol.Name
|
||||
}
|
||||
stack = append(stack, DebugStackFrame{
|
||||
Function: name,
|
||||
Level: i,
|
||||
})
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
// DebugLocals returns local variables for the current frame.
|
||||
func (t *Thread) DebugLocals() []DebugVarInfo {
|
||||
if t.curFrame == nil {
|
||||
return nil
|
||||
}
|
||||
var vars []DebugVarInfo
|
||||
for i := 0; i < t.curFrame.localCount; i++ {
|
||||
idx := t.curFrame.localBase + i
|
||||
if idx < len(t.locals) {
|
||||
scope := "LOCAL"
|
||||
if i < t.curFrame.paramCount {
|
||||
scope = "PARAM"
|
||||
}
|
||||
vars = append(vars, DebugVarInfo{
|
||||
Name: fmt.Sprintf("_%d", i+1), // placeholder name
|
||||
Value: t.locals[idx],
|
||||
Scope: scope,
|
||||
Index: i + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
// currentFuncName returns the name of the currently executing function.
|
||||
func (t *Thread) currentFuncName() string {
|
||||
if t.callSP > 0 {
|
||||
frame := &t.calls[t.callSP-1]
|
||||
if frame.symbol != nil {
|
||||
return frame.symbol.Name
|
||||
}
|
||||
}
|
||||
return "MAIN"
|
||||
}
|
||||
253
hbrt/debugcli.go
Normal file
253
hbrt/debugcli.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Interactive CLI debugger for Five.
|
||||
// Provides a gdb-like command interface for stepping through PRG code.
|
||||
//
|
||||
// Commands:
|
||||
// s, step — step to next line
|
||||
// n, next — step over (don't enter functions)
|
||||
// o, out — step out of current function
|
||||
// c, cont — continue (run until breakpoint)
|
||||
// b <line> — set breakpoint at line
|
||||
// d <n> — delete breakpoint
|
||||
// bl — list breakpoints
|
||||
// p <expr> — print variable value
|
||||
// l — list locals
|
||||
// bt — show call stack (backtrace)
|
||||
// q — quit
|
||||
// h — help
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Terminal mode helpers — restore cooked mode for debugger, re-enter raw for program
|
||||
var savedTermios syscall.Termios
|
||||
var termSaved bool
|
||||
|
||||
func restoreCooked() {
|
||||
fd := int(os.Stdin.Fd())
|
||||
var t syscall.Termios
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
if !termSaved {
|
||||
savedTermios = t
|
||||
termSaved = true
|
||||
}
|
||||
// Set cooked mode
|
||||
t.Lflag |= syscall.ICANON | syscall.ECHO
|
||||
t.Oflag |= syscall.OPOST
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
}
|
||||
|
||||
func reenterRaw() {
|
||||
fd := int(os.Stdin.Fd())
|
||||
var t syscall.Termios
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
t.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG
|
||||
t.Oflag &^= syscall.OPOST
|
||||
t.Cc[syscall.VMIN] = 1
|
||||
t.Cc[syscall.VTIME] = 0
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
}
|
||||
|
||||
// CLIDebugger creates a DebugCallback for interactive terminal debugging.
|
||||
func CLIDebugger() DebugCallback {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
lastCmd := "s" // default repeat command
|
||||
|
||||
return func(event *DebugEvent) int {
|
||||
// Restore terminal to cooked mode for debugger I/O
|
||||
fmt.Print("\r\n")
|
||||
restoreCooked()
|
||||
defer reenterRaw()
|
||||
|
||||
if event.Reason == "breakpoint" {
|
||||
fmt.Printf(" ** Breakpoint at %s:%d\n", event.Module, event.Line)
|
||||
}
|
||||
fmt.Printf(" %s:%d in %s()\n", event.Module, event.Line, event.Function)
|
||||
|
||||
// Show source line if available
|
||||
showSourceLine(event.Module, event.Line)
|
||||
|
||||
for {
|
||||
fmt.Printf("(dbg) ")
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return DbgContinue
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
line = lastCmd // repeat last command
|
||||
} else {
|
||||
lastCmd = line
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
cmd := parts[0]
|
||||
|
||||
switch cmd {
|
||||
case "s", "step":
|
||||
return DbgStepLine
|
||||
|
||||
case "n", "next":
|
||||
return DbgStepOver
|
||||
|
||||
case "o", "out":
|
||||
return DbgStepOut
|
||||
|
||||
case "c", "cont", "continue":
|
||||
return DbgContinue
|
||||
|
||||
case "b", "break":
|
||||
if len(parts) >= 2 {
|
||||
lineNo, err := strconv.Atoi(parts[1])
|
||||
if err == nil {
|
||||
mod := event.Module
|
||||
if len(parts) >= 3 {
|
||||
mod = parts[2]
|
||||
}
|
||||
dbg := event.Thread.VM().Debugger
|
||||
idx := dbg.AddBreakpoint(mod, lineNo)
|
||||
fmt.Printf(" Breakpoint %d at %s:%d\n", idx, mod, lineNo)
|
||||
} else {
|
||||
fmt.Println(" Usage: b <line> [module]")
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" Usage: b <line> [module]")
|
||||
}
|
||||
|
||||
case "d", "del", "delete":
|
||||
if len(parts) >= 2 {
|
||||
idx, err := strconv.Atoi(parts[1])
|
||||
if err == nil {
|
||||
event.Thread.VM().Debugger.RemoveBreakpoint(idx)
|
||||
fmt.Printf(" Breakpoint %d removed\n", idx)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" Usage: d <breakpoint_number>")
|
||||
}
|
||||
|
||||
case "bl", "breakpoints":
|
||||
dbg := event.Thread.VM().Debugger
|
||||
if len(dbg.Breakpoints) == 0 {
|
||||
fmt.Println(" No breakpoints")
|
||||
} else {
|
||||
for i, bp := range dbg.Breakpoints {
|
||||
status := "ON "
|
||||
if !bp.Enabled {
|
||||
status = "OFF"
|
||||
}
|
||||
fmt.Printf(" %d: [%s] %s:%d (hits: %d)\n", i, status, bp.Module, bp.Line, bp.HitCount)
|
||||
}
|
||||
}
|
||||
|
||||
case "l", "locals":
|
||||
if len(event.Locals) == 0 {
|
||||
fmt.Println(" No local variables")
|
||||
} else {
|
||||
for _, v := range event.Locals {
|
||||
fmt.Printf(" %s [%s] %s = %s\n", v.Scope, fmt.Sprintf("%d", v.Index), v.Name, v.Value.String())
|
||||
}
|
||||
}
|
||||
|
||||
case "p", "print":
|
||||
if len(parts) >= 2 {
|
||||
varName := parts[1]
|
||||
found := false
|
||||
for _, v := range event.Locals {
|
||||
if strings.EqualFold(v.Name, varName) || fmt.Sprintf("_%d", v.Index) == varName {
|
||||
fmt.Printf(" %s = %s\n", v.Name, v.Value.String())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// Try by index
|
||||
idx, err := strconv.Atoi(varName)
|
||||
if err == nil && idx >= 1 && idx <= len(event.Locals) {
|
||||
v := event.Locals[idx-1]
|
||||
fmt.Printf(" %s = %s\n", v.Name, v.Value.String())
|
||||
} else {
|
||||
fmt.Printf(" Variable '%s' not found\n", varName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" Usage: p <varname|index>")
|
||||
}
|
||||
|
||||
case "bt", "backtrace", "stack":
|
||||
if len(event.CallStack) == 0 {
|
||||
fmt.Println(" Empty call stack")
|
||||
} else {
|
||||
for i, frame := range event.CallStack {
|
||||
marker := " "
|
||||
if i == 0 {
|
||||
marker = "=>"
|
||||
}
|
||||
if frame.Module != "" {
|
||||
fmt.Printf(" %s #%d %s() at %s:%d\n", marker, frame.Level, frame.Function, frame.Module, frame.Line)
|
||||
} else {
|
||||
fmt.Printf(" %s #%d %s()\n", marker, frame.Level, frame.Function)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "q", "quit":
|
||||
fmt.Println(" Debugger quit.")
|
||||
os.Exit(0)
|
||||
|
||||
case "h", "help", "?":
|
||||
fmt.Println(" Five Debugger Commands:")
|
||||
fmt.Println(" s, step — step to next line")
|
||||
fmt.Println(" n, next — step over function calls")
|
||||
fmt.Println(" o, out — step out of current function")
|
||||
fmt.Println(" c, cont — continue (run to next breakpoint)")
|
||||
fmt.Println(" b <line> — set breakpoint at line")
|
||||
fmt.Println(" d <n> — delete breakpoint n")
|
||||
fmt.Println(" bl — list all breakpoints")
|
||||
fmt.Println(" l — show local variables")
|
||||
fmt.Println(" p <var> — print variable value")
|
||||
fmt.Println(" bt — show call stack")
|
||||
fmt.Println(" q — quit")
|
||||
|
||||
default:
|
||||
fmt.Printf(" Unknown command: %s (type 'h' for help)\n", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showSourceLine attempts to show the source code around the current line.
|
||||
func showSourceLine(module string, line int) {
|
||||
data, err := os.ReadFile(module)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
start := line - 3
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
end := line + 2
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
marker := " "
|
||||
if i == line {
|
||||
marker = ">>"
|
||||
}
|
||||
if i-1 < len(lines) {
|
||||
fmt.Printf(" %s %4d: %s\n", marker, i, lines[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
334
hbrt/debugtui.go
Normal file
334
hbrt/debugtui.go
Normal file
@@ -0,0 +1,334 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Full-screen TUI debugger for Five — Harbour/Clipper debugger style.
|
||||
// Uses ANSI escape codes for terminal rendering.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// TUIDebugger creates a full-screen terminal debugger callback.
|
||||
func TUIDebugger() DebugCallback {
|
||||
var sourceCache map[string][]string // file → lines cache
|
||||
sourceCache = make(map[string][]string)
|
||||
|
||||
return func(event *DebugEvent) int {
|
||||
// Switch to cooked mode for debugger
|
||||
restoreCooked()
|
||||
defer reenterRaw()
|
||||
|
||||
// Load source file
|
||||
srcDir := ""
|
||||
if event.Thread.VM().Debugger != nil {
|
||||
srcDir = event.Thread.VM().Debugger.SourceDir
|
||||
}
|
||||
lines := loadSource(sourceCache, event.Module, srcDir)
|
||||
|
||||
// Get terminal size
|
||||
rows, cols := termSize()
|
||||
if rows < 10 {
|
||||
rows = 24
|
||||
}
|
||||
if cols < 40 {
|
||||
cols = 80
|
||||
}
|
||||
|
||||
// Layout:
|
||||
// Row 1: title bar
|
||||
// Row 2 ~ srcEnd: source code
|
||||
// srcEnd+1 ~ panelEnd: locals + stack side by side
|
||||
// Last row: command bar
|
||||
srcHeight := rows - 10
|
||||
if srcHeight < 5 {
|
||||
srcHeight = 5
|
||||
}
|
||||
panelHeight := rows - srcHeight - 3 // title + src + cmdbar
|
||||
if panelHeight < 3 {
|
||||
panelHeight = 3
|
||||
}
|
||||
|
||||
for {
|
||||
// Clear screen
|
||||
fmt.Print("\033[2J\033[H")
|
||||
|
||||
// === Title Bar ===
|
||||
title := fmt.Sprintf(" Five Debugger - %s:%d %s() ", event.Module, event.Line, event.Function)
|
||||
if event.Reason == "breakpoint" {
|
||||
title += "[BREAKPOINT] "
|
||||
}
|
||||
fmt.Printf("\033[7m%-*s\033[0m\r\n", cols, title)
|
||||
|
||||
// === Source Window ===
|
||||
drawSourceWindow(lines, event.Line, srcHeight, cols, event.Thread.VM().Debugger)
|
||||
|
||||
// === Panels: Locals | Call Stack ===
|
||||
localW := cols / 2
|
||||
stackW := cols - localW
|
||||
drawPanels(event, panelHeight, localW, stackW)
|
||||
|
||||
// === Command Bar ===
|
||||
fmt.Printf("\033[7m%-*s\033[0m", cols,
|
||||
" F5:Go F7:Into F8:Step F9:Break F10:Over F11:Out L:Locals ESC:Quit")
|
||||
|
||||
// Wait for key
|
||||
key := readDebugKey()
|
||||
|
||||
switch key {
|
||||
case 0x1B, 'q', 'Q': // ESC or Q — quit
|
||||
fmt.Print("\033[2J\033[H")
|
||||
restoreCooked()
|
||||
os.Exit(0)
|
||||
|
||||
case 0xF5, 'g', 'G': // F5 or G — Go/Continue
|
||||
return DbgContinue
|
||||
|
||||
case 0xF7: // F7 — Step Into
|
||||
return DbgStepLine
|
||||
|
||||
case 0xF8, 's', 'S', 10, 13: // F8 / s / Enter — Step
|
||||
return DbgStepLine
|
||||
|
||||
case 0xF9, 'b', 'B': // F9 or B — Toggle Breakpoint
|
||||
dbg := event.Thread.VM().Debugger
|
||||
found := false
|
||||
for i, bp := range dbg.Breakpoints {
|
||||
if bp.Line == event.Line {
|
||||
dbg.RemoveBreakpoint(i)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
dbg.AddBreakpoint(event.Module, event.Line)
|
||||
}
|
||||
continue // redraw
|
||||
|
||||
case 0xFA, 'n', 'N': // F10 or N — Step Over
|
||||
return DbgStepOver
|
||||
|
||||
case 0xFB, 'o', 'O': // F11 or O — Step Out
|
||||
return DbgStepOut
|
||||
|
||||
case 'c', 'C': // Continue
|
||||
return DbgContinue
|
||||
|
||||
case 0xE0, 0xE1, 0xE2, 0xE3: // Arrow keys — ignore
|
||||
continue
|
||||
|
||||
default:
|
||||
continue // unknown key, redraw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawSourceWindow(lines []string, curLine, height, width int, dbg *Debugger) {
|
||||
// Calculate visible range centered on current line
|
||||
start := curLine - height/2
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
end := start + height - 1
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
|
||||
// Top border
|
||||
fmt.Printf("\033[36m\u250C\u2500 Source \u2500%s\u2510\033[0m\r\n", strings.Repeat("\u2500", width-12))
|
||||
|
||||
for i := start; i <= end; i++ {
|
||||
lineText := ""
|
||||
if i-1 < len(lines) {
|
||||
lineText = lines[i-1]
|
||||
}
|
||||
// Truncate to fit
|
||||
if len(lineText) > width-10 {
|
||||
lineText = lineText[:width-10]
|
||||
}
|
||||
|
||||
// Breakpoint marker
|
||||
bpMark := " "
|
||||
if dbg != nil {
|
||||
for _, bp := range dbg.Breakpoints {
|
||||
if bp.Enabled && bp.Line == i {
|
||||
bpMark = "\033[31m\u25CF\033[0m" // red dot
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current line marker
|
||||
if i == curLine {
|
||||
fmt.Printf("\033[36m\u2502\033[0m%s\033[33m>> %4d:\033[0m \033[7m%-*s\033[0m\033[36m\u2502\033[0m\r\n",
|
||||
bpMark, i, width-10, lineText)
|
||||
} else {
|
||||
fmt.Printf("\033[36m\u2502\033[0m%s %4d: %-*s\033[36m\u2502\033[0m\r\n",
|
||||
bpMark, i, width-10, lineText)
|
||||
}
|
||||
}
|
||||
|
||||
// Pad remaining lines
|
||||
for i := end - start + 1; i < height; i++ {
|
||||
fmt.Printf("\033[36m\u2502\033[0m%*s\033[36m\u2502\033[0m\r\n", width-2, "")
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
fmt.Printf("\033[36m\u2514%s\u2518\033[0m\r\n", strings.Repeat("\u2500", width-2))
|
||||
}
|
||||
|
||||
func drawPanels(event *DebugEvent, height, localW, stackW int) {
|
||||
// Headers
|
||||
localHeader := fmt.Sprintf("\u250C\u2500 Locals %s\u2510", strings.Repeat("\u2500", localW-11))
|
||||
stackHeader := fmt.Sprintf("\u250C\u2500 Stack %s\u2510", strings.Repeat("\u2500", stackW-10))
|
||||
fmt.Printf("\033[36m%s%s\033[0m\r\n", localHeader, stackHeader)
|
||||
|
||||
// Content rows
|
||||
for i := 0; i < height-2; i++ {
|
||||
// Left: locals
|
||||
localLine := ""
|
||||
if i < len(event.Locals) {
|
||||
v := event.Locals[i]
|
||||
val := v.Value.String()
|
||||
if len(val) > localW-8 {
|
||||
val = val[:localW-11] + "..."
|
||||
}
|
||||
localLine = fmt.Sprintf(" %s = %s", v.Name, val)
|
||||
}
|
||||
if len(localLine) > localW-2 {
|
||||
localLine = localLine[:localW-2]
|
||||
}
|
||||
|
||||
// Right: call stack
|
||||
stackLine := ""
|
||||
if i < len(event.CallStack) {
|
||||
f := event.CallStack[i]
|
||||
if f.Module != "" {
|
||||
stackLine = fmt.Sprintf(" %s() %s:%d", f.Function, f.Module, f.Line)
|
||||
} else {
|
||||
stackLine = fmt.Sprintf(" %s()", f.Function)
|
||||
}
|
||||
}
|
||||
if len(stackLine) > stackW-2 {
|
||||
stackLine = stackLine[:stackW-2]
|
||||
}
|
||||
|
||||
fmt.Printf("\033[36m\u2502\033[0m%-*s\033[36m\u2502\033[0m%-*s\033[36m\u2502\033[0m\r\n",
|
||||
localW-2, localLine, stackW-2, stackLine)
|
||||
}
|
||||
|
||||
// Bottom borders
|
||||
localFooter := fmt.Sprintf("\u2514%s\u2518", strings.Repeat("\u2500", localW-2))
|
||||
stackFooter := fmt.Sprintf("\u2514%s\u2518", strings.Repeat("\u2500", stackW-2))
|
||||
fmt.Printf("\033[36m%s%s\033[0m\r\n", localFooter, stackFooter)
|
||||
}
|
||||
|
||||
func loadSource(cache map[string][]string, filename string, sourceDir string) []string {
|
||||
if lines, ok := cache[filename]; ok {
|
||||
return lines
|
||||
}
|
||||
|
||||
// Try as-is first
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil && sourceDir != "" {
|
||||
// Try relative to source directory
|
||||
joined := sourceDir + "/" + filename
|
||||
data, err = os.ReadFile(joined)
|
||||
if err != nil {
|
||||
// Try just the basename in source dir
|
||||
base := filename
|
||||
if idx := strings.LastIndexAny(filename, "/\\"); idx >= 0 {
|
||||
base = filename[idx+1:]
|
||||
}
|
||||
data, err = os.ReadFile(sourceDir + "/" + base)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return []string{"(source not available: " + filename + ")"}
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
cache[filename] = lines
|
||||
return lines
|
||||
}
|
||||
|
||||
func termSize() (int, int) {
|
||||
type winsize struct {
|
||||
Row, Col, Xpixel, Ypixel uint16
|
||||
}
|
||||
var ws winsize
|
||||
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, uintptr(1),
|
||||
uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
|
||||
return int(ws.Row), int(ws.Col)
|
||||
}
|
||||
|
||||
// readDebugKey reads a key in raw mode for the debugger.
|
||||
// Returns ASCII for normal keys, 0xF5-0xFB for F5-F11.
|
||||
func readDebugKey() int {
|
||||
// Temporarily set raw mode for key reading
|
||||
fd := int(os.Stdin.Fd())
|
||||
var t syscall.Termios
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
raw := t
|
||||
raw.Lflag &^= syscall.ICANON | syscall.ECHO
|
||||
raw.Cc[syscall.VMIN] = 1
|
||||
raw.Cc[syscall.VTIME] = 0
|
||||
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&raw)), 0, 0, 0)
|
||||
defer syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
||||
|
||||
buf := make([]byte, 8)
|
||||
n, _ := syscall.Read(fd, buf)
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ESC sequence
|
||||
if buf[0] == 0x1B {
|
||||
if n == 1 {
|
||||
return 0x1B // bare ESC
|
||||
}
|
||||
if n >= 3 && buf[1] == '[' {
|
||||
// Arrow keys: ESC [ A/B/C/D
|
||||
switch buf[2] {
|
||||
case 'A':
|
||||
return 0xE0 // Up
|
||||
case 'B':
|
||||
return 0xE1 // Down
|
||||
case 'C':
|
||||
return 0xE2 // Right
|
||||
case 'D':
|
||||
return 0xE3 // Left
|
||||
}
|
||||
// F5-F11: ESC [ 1 5 ~ through ESC [ 2 4 ~
|
||||
if n >= 4 && buf[n-1] == '~' {
|
||||
code := string(buf[2 : n-1])
|
||||
switch code {
|
||||
case "15":
|
||||
return 0xF5 // F5
|
||||
case "17":
|
||||
return 0xF6 // F6
|
||||
case "18":
|
||||
return 0xF7 // F7
|
||||
case "19":
|
||||
return 0xF8 // F8
|
||||
case "20":
|
||||
return 0xF9 // F9
|
||||
case "21":
|
||||
return 0xFA // F10
|
||||
case "23":
|
||||
return 0xFB // F11
|
||||
case "24":
|
||||
return 0xFC // F12
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0 // ignore unknown ESC sequences (don't quit)
|
||||
}
|
||||
|
||||
return int(buf[0])
|
||||
}
|
||||
307
hbrt/frb.go
Normal file
307
hbrt/frb.go
Normal file
@@ -0,0 +1,307 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// FRB (Five Runtime Binary) — dynamic module loading.
|
||||
//
|
||||
// Unlike Harbour's HRB (pcode interpreter), Five FRB compiles PRG to native
|
||||
// Go shared library (.so/.dll) for full native speed execution.
|
||||
//
|
||||
// FRB file format:
|
||||
// Magic: 0xC0 'F' 'R' 'B' (4 bytes)
|
||||
// Version: uint16 LE (2 bytes) — currently 1
|
||||
// Flags: uint16 LE (2 bytes)
|
||||
// SymCount: uint32 LE (4 bytes)
|
||||
// Symbols: []{Name: null-terminated, Scope: byte}
|
||||
// SharedLib: remaining bytes = embedded .so/.dll binary
|
||||
//
|
||||
// Usage from PRG:
|
||||
// pMod := FrbLoad("mymodule.frb")
|
||||
// FrbDo(pMod, "MYFUNC", arg1, arg2)
|
||||
// xResult := FrbDo(pMod, "CALCULATE", 42)
|
||||
// FrbUnload(pMod)
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FRB magic bytes
|
||||
var frbMagic = []byte{0xC0, 'F', 'R', 'B'}
|
||||
|
||||
const frbVersion = 2
|
||||
|
||||
// FRB mode flags
|
||||
const (
|
||||
FrbModeNative byte = 0x01 // Go plugin (.so)
|
||||
FrbModePcode byte = 0x02 // Five pcode (interpreter)
|
||||
)
|
||||
|
||||
// FrbModule represents a loaded FRB module.
|
||||
// FRB binding modes (how module symbols interact with VM globals)
|
||||
const (
|
||||
FrbBindDefault = 0 // Module-local; only accessible via FrbDo()
|
||||
FrbBindOverload = 1 // Overwrite existing VM symbols
|
||||
FrbBindExport = 2 // Register in VM but don't overwrite existing
|
||||
)
|
||||
|
||||
type FrbModule struct {
|
||||
Name string
|
||||
LocalSyms map[string]*Symbol // module-scoped symbols (isolated)
|
||||
Plugin *plugin.Plugin // Go plugin handle (native mode)
|
||||
TempDir string // temp dir for extracted .so
|
||||
BindMode int // how symbols are registered
|
||||
VM *VM // owning VM
|
||||
Registered []string // names registered in VM (for unload cleanup)
|
||||
OldSyms map[string]*Symbol // previous symbols overwritten (for restore)
|
||||
}
|
||||
|
||||
// FindFunc looks up a function in this module's local scope.
|
||||
// Main() is always module-local and never leaks to the host VM.
|
||||
func (m *FrbModule) FindFunc(name string) func(*Thread) {
|
||||
if m.LocalSyms != nil {
|
||||
if sym, ok := m.LocalSyms[name]; ok && sym.Func != nil {
|
||||
return sym.Func
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FrbBuild compiles a PRG file to FRB format.
|
||||
// Steps: PRG → gengo → Go source → go build -buildmode=plugin → FRB
|
||||
func FrbBuild(prgFile, outputFile string, fiveExe string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return fmt.Errorf("FRB plugins not supported on Windows (Go plugin limitation)")
|
||||
}
|
||||
|
||||
// 1. Generate Go source
|
||||
tmpDir, err := os.MkdirTemp("", "frb-build-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Run five gen to produce Go source
|
||||
genCmd := exec.Command(fiveExe, "gen", prgFile)
|
||||
goSrc, err := genCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gen failed: %w", err)
|
||||
}
|
||||
|
||||
// Modify source: change package main → package main (plugin compatible)
|
||||
goSrcStr := string(goSrc)
|
||||
goSrcStr = strings.Replace(goSrcStr, "func main() {", "// FRB module — no main()", 1)
|
||||
|
||||
// Add plugin exports
|
||||
goSrcStr += `
|
||||
// FRB plugin exports
|
||||
var FRB_Symbols = symbols
|
||||
`
|
||||
|
||||
goFile := filepath.Join(tmpDir, "frb_module.go")
|
||||
if err := os.WriteFile(goFile, []byte(goSrcStr), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Build as Go plugin
|
||||
soFile := filepath.Join(tmpDir, "module.so")
|
||||
buildCmd := exec.Command("go", "build", "-buildmode=plugin", "-o", soFile, goFile)
|
||||
buildCmd.Dir = tmpDir
|
||||
if output, err := buildCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("build failed: %s\n%w", string(output), err)
|
||||
}
|
||||
|
||||
// 3. Package as FRB
|
||||
soData, err := os.ReadFile(soFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write header
|
||||
f.Write(frbMagic)
|
||||
binary.Write(f, binary.LittleEndian, uint16(frbVersion))
|
||||
binary.Write(f, binary.LittleEndian, uint16(0)) // flags
|
||||
|
||||
// Write symbol count (placeholder — extracted from Go source)
|
||||
binary.Write(f, binary.LittleEndian, uint32(0))
|
||||
|
||||
// Write embedded .so
|
||||
f.Write(soData)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FrbLoad loads an FRB module from file.
|
||||
func FrbLoad(vm *VM, filename string) (*FrbModule, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate magic
|
||||
if len(data) < 12 || data[0] != 0xC0 || data[1] != 'F' || data[2] != 'R' || data[3] != 'B' {
|
||||
return nil, fmt.Errorf("invalid FRB file: bad magic")
|
||||
}
|
||||
|
||||
version := binary.LittleEndian.Uint16(data[4:6])
|
||||
_ = version
|
||||
mode := data[6] // flags byte 1 = mode
|
||||
|
||||
// Pcode mode — use interpreter, no Go needed
|
||||
if mode == FrbModePcode {
|
||||
return frbLoadPcode(vm, data[12:], filename)
|
||||
}
|
||||
|
||||
// Native mode — load Go plugin
|
||||
soData := data[12:]
|
||||
|
||||
// Extract .so to temp file
|
||||
tmpDir, err := os.MkdirTemp("", "frb-load-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
soFile := filepath.Join(tmpDir, "module.so")
|
||||
if err := os.WriteFile(soFile, soData, 0755); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Snapshot current symbols before loading
|
||||
oldSymNames := vm.SymbolNames()
|
||||
|
||||
// Load as Go plugin — init() auto-registers symbols via RegisterLibModule
|
||||
p, err := plugin.Open(soFile)
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("plugin load failed: %w", err)
|
||||
}
|
||||
|
||||
// Register any lib modules that were added by the plugin's init()
|
||||
vm.RegisterLibModules()
|
||||
|
||||
// Determine which symbols were added by the plugin
|
||||
frbMod := &FrbModule{
|
||||
Name: filename,
|
||||
LocalSyms: make(map[string]*Symbol),
|
||||
OldSyms: make(map[string]*Symbol),
|
||||
Plugin: p,
|
||||
TempDir: tmpDir,
|
||||
VM: vm,
|
||||
}
|
||||
|
||||
newSymNames := vm.SymbolNames()
|
||||
for _, name := range newSymNames {
|
||||
if !containsStr(oldSymNames, name) {
|
||||
sym := vm.FindSymbol(name)
|
||||
if sym != nil {
|
||||
frbMod.LocalSyms[name] = sym
|
||||
frbMod.Registered = append(frbMod.Registered, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frbMod, nil
|
||||
}
|
||||
|
||||
// frbLoadPcode loads a pcode-mode FRB.
|
||||
func frbLoadPcode(vm *VM, data []byte, filename string) (*FrbModule, error) {
|
||||
pcMod, err := DeserializePcodeModule(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pcode parse failed: %w", err)
|
||||
}
|
||||
|
||||
frbMod := &FrbModule{
|
||||
Name: filename,
|
||||
LocalSyms: make(map[string]*Symbol),
|
||||
OldSyms: make(map[string]*Symbol),
|
||||
BindMode: FrbBindDefault,
|
||||
VM: vm,
|
||||
}
|
||||
|
||||
// Build module-local symbols
|
||||
for name, fn := range pcMod.Funcs {
|
||||
pcFn := fn
|
||||
pcModRef := pcMod
|
||||
goFunc := func(t *Thread) {
|
||||
ExecPcode(t, pcFn, pcModRef)
|
||||
}
|
||||
frbMod.LocalSyms[name] = &Symbol{
|
||||
Name: name,
|
||||
Scope: FsPublic | FsLocal,
|
||||
Func: goFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// Register non-Main symbols in VM (save old for restore on unload)
|
||||
for name, sym := range frbMod.LocalSyms {
|
||||
if name == "MAIN" {
|
||||
continue // Main is always module-local
|
||||
}
|
||||
old := vm.FindSymbol(name)
|
||||
if old != nil {
|
||||
// Default mode: don't overwrite existing host functions
|
||||
// Module function accessible via FrbDo() only
|
||||
frbMod.OldSyms[name] = old
|
||||
continue
|
||||
}
|
||||
// New symbol — register globally
|
||||
vm.RegisterSymbol(sym)
|
||||
frbMod.Registered = append(frbMod.Registered, name)
|
||||
}
|
||||
|
||||
return frbMod, nil
|
||||
}
|
||||
|
||||
// FrbUnload unloads an FRB module.
|
||||
// Removes registered symbols from VM and restores any overwritten ones.
|
||||
func FrbUnload(mod *FrbModule) {
|
||||
if mod == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Restore VM symbols
|
||||
if mod.VM != nil {
|
||||
for _, name := range mod.Registered {
|
||||
if old, exists := mod.OldSyms[name]; exists {
|
||||
// Restore previous symbol
|
||||
mod.VM.RegisterSymbol(old)
|
||||
} else {
|
||||
// Remove symbol that didn't exist before
|
||||
mod.VM.UnregisterSymbol(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temp files
|
||||
if mod.TempDir != "" {
|
||||
os.RemoveAll(mod.TempDir)
|
||||
}
|
||||
|
||||
// Clear references
|
||||
mod.LocalSyms = nil
|
||||
mod.OldSyms = nil
|
||||
mod.Registered = nil
|
||||
}
|
||||
|
||||
func containsStr(slice []string, s string) bool {
|
||||
for _, v := range slice {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
232
hbrt/frbmem.go
Normal file
232
hbrt/frbmem.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// FRB in-memory compilation — compile PRG source at runtime and execute.
|
||||
// This is Five's equivalent of Harbour's hb_compileFromBuf() + hb_hrbRun().
|
||||
//
|
||||
// Usage from PRG:
|
||||
// pMod := FrbCompile(cPrgSource) // compile PRG string → FRB in memory
|
||||
// result := FrbDo(pMod, "MYFUNC", args) // call compiled function
|
||||
// FrbUnload(pMod)
|
||||
//
|
||||
// // Or one-shot:
|
||||
// result := FrbExec(cPrgSource) // compile + run Main() + unload
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
)
|
||||
|
||||
// FrbCompileSource compiles PRG source code to an FRB module in memory.
|
||||
// If Go compiler is available, uses native plugin mode.
|
||||
// If not, falls back to pcode interpreter mode (--pcode).
|
||||
func FrbCompileSource(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) {
|
||||
// Check if Go is available
|
||||
if !isGoAvailable() {
|
||||
return frbCompilePcode(vm, prgSource, fiveExe)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "frb-mem-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write PRG source to temp file with unique name
|
||||
prgFile := filepath.Join(tmpDir, fmt.Sprintf("dynamic_%d.prg", frbSeq))
|
||||
frbSeq++
|
||||
if err := os.WriteFile(prgFile, []byte(prgSource), 0644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find five executable
|
||||
if fiveExe == "" {
|
||||
fiveExe, _ = os.Executable()
|
||||
}
|
||||
|
||||
// Compile PRG → FRB using five frb command
|
||||
frbFile := filepath.Join(tmpDir, "dynamic.frb")
|
||||
cmd := exec.Command(fiveExe, "frb", prgFile, "-o", frbFile)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("compile failed: %s\n%w", string(output), err)
|
||||
}
|
||||
|
||||
// Load FRB
|
||||
mod, err := FrbLoad(vm, frbFile)
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, err
|
||||
}
|
||||
// Override TempDir to clean up everything
|
||||
mod.TempDir = tmpDir
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
// FrbCompileDirect compiles PRG source directly to a Go plugin without
|
||||
// going through the five CLI. Uses the compiler packages directly.
|
||||
// This is faster than FrbCompileSource for hot compilation.
|
||||
func FrbCompileDirect(vm *VM, prgSource string) (*FrbModule, error) {
|
||||
tmpDir, err := os.MkdirTemp("", "frb-direct-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need the Five project root for go.mod replace directive
|
||||
fiveRoot := findFiveRoot()
|
||||
if fiveRoot == "" {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("cannot find Five project root (go.mod)")
|
||||
}
|
||||
|
||||
// Write Go source — import compiler packages inline
|
||||
// This uses exec to run a helper that does the compilation
|
||||
helperSrc := fmt.Sprintf(`package main
|
||||
import (
|
||||
"five/compiler/gengo"
|
||||
"five/compiler/parser"
|
||||
"five/compiler/pp"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
func main() {
|
||||
source := %q
|
||||
pre := pp.New()
|
||||
processed, _ := pre.Process("dynamic.prg", source)
|
||||
file, errs := parser.Parse("dynamic.prg", processed)
|
||||
if len(errs) > 0 {
|
||||
for _, e := range errs { fmt.Fprintln(os.Stderr, e) }
|
||||
os.Exit(1)
|
||||
}
|
||||
goSrc := gengo.GenerateLibrary(file)
|
||||
fmt.Print(goSrc)
|
||||
}
|
||||
`, prgSource)
|
||||
|
||||
helperFile := filepath.Join(tmpDir, "helper.go")
|
||||
os.WriteFile(helperFile, []byte(helperSrc), 0644)
|
||||
|
||||
// Write go.mod for helper
|
||||
goMod := fmt.Sprintf("module frbhelper\n\ngo 1.21\n\nrequire five v0.0.0\nreplace five => %s\n", fiveRoot)
|
||||
os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644)
|
||||
|
||||
// Run go mod tidy + generate
|
||||
tidyCmd := exec.Command("go", "mod", "tidy")
|
||||
tidyCmd.Dir = tmpDir
|
||||
tidyCmd.CombinedOutput()
|
||||
|
||||
genCmd := exec.Command("go", "run", "helper.go")
|
||||
genCmd.Dir = tmpDir
|
||||
goSrcBytes, err := genCmd.Output()
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("codegen failed: %w", err)
|
||||
}
|
||||
|
||||
// Write generated module.go
|
||||
os.WriteFile(filepath.Join(tmpDir, "module.go"), goSrcBytes, 0644)
|
||||
os.Remove(helperFile) // remove helper, keep module.go
|
||||
|
||||
// Build plugin
|
||||
soFile := filepath.Join(tmpDir, "module.so")
|
||||
buildCmd := exec.Command("go", "build", "-buildmode=plugin", "-o", soFile, "module.go")
|
||||
buildCmd.Dir = tmpDir
|
||||
if output, err := buildCmd.CombinedOutput(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("plugin build failed: %s\n%w", string(output), err)
|
||||
}
|
||||
|
||||
// Load plugin
|
||||
p, err := plugin.Open(soFile)
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("plugin load failed: %w", err)
|
||||
}
|
||||
|
||||
vm.RegisterLibModules()
|
||||
|
||||
return &FrbModule{
|
||||
Name: "<dynamic>",
|
||||
Plugin: p,
|
||||
TempDir: tmpDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// findFiveRoot locates the Five project root by searching for go.mod
|
||||
func findFiveRoot() string {
|
||||
// Try executable location first
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
for d := dir; ; d = filepath.Dir(d) {
|
||||
if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil {
|
||||
return d
|
||||
}
|
||||
if d == filepath.Dir(d) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try current directory
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
for d := cwd; ; d = filepath.Dir(d) {
|
||||
if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil {
|
||||
return d
|
||||
}
|
||||
if d == filepath.Dir(d) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var frbSeq int // sequence number for unique module names
|
||||
|
||||
// isGoAvailable checks if the Go compiler is installed.
|
||||
func isGoAvailable() bool {
|
||||
for _, p := range []string{"go", "/usr/local/go/bin/go", "/usr/bin/go"} {
|
||||
if _, err := exec.LookPath(p); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// frbCompilePcode compiles PRG source to pcode FRB (no Go needed).
|
||||
func frbCompilePcode(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) {
|
||||
tmpDir, err := os.MkdirTemp("", "frb-pcode-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prgFile := filepath.Join(tmpDir, fmt.Sprintf("dynamic_%d.prg", frbSeq))
|
||||
frbSeq++
|
||||
os.WriteFile(prgFile, []byte(prgSource), 0644)
|
||||
|
||||
frbFile := filepath.Join(tmpDir, "dynamic.frb")
|
||||
cmd := exec.Command(fiveExe, "frb", prgFile, "-o", frbFile, "--pcode")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("pcode compile failed: %s\n%w", string(output), err)
|
||||
}
|
||||
|
||||
mod, err := FrbLoad(vm, frbFile)
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, err
|
||||
}
|
||||
mod.TempDir = tmpDir
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
var _ = binary.LittleEndian // keep import
|
||||
452
hbrt/gobridge.go
Normal file
452
hbrt/gobridge.go
Normal file
@@ -0,0 +1,452 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// gobridge.go — Bridge between Harbour Values and native Go objects.
|
||||
//
|
||||
// Allows PRG code to hold and manipulate Go objects (sql.DB, http.Client, etc.)
|
||||
// using Harbour's object syntax: obj:Method(args)
|
||||
//
|
||||
// Architecture:
|
||||
// PRG: db := sql.Open("sqlite", ":memory:") → Value wrapping *sql.DB
|
||||
// PRG: db:Exec("CREATE TABLE ...") → reflect.Call on *sql.DB.Exec
|
||||
// PRG: db:Close() → reflect.Call on *sql.DB.Close
|
||||
//
|
||||
// Type coercion: Value ↔ Go types automatic conversion
|
||||
// string ↔ Value.AsString()
|
||||
// int ↔ Value.AsInt()
|
||||
// float64 ↔ Value.AsNumDouble()
|
||||
// bool ↔ Value.AsBool()
|
||||
// error ↔ Value (string of error message, or NIL)
|
||||
// nil ↔ Value.IsNil()
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoValue — wraps any Go object inside a Harbour Value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// WrapGo wraps any Go value (pointer, interface, etc.) as a Harbour Value.
|
||||
// The Go object is stored in Value.ptr as interface{}.
|
||||
func WrapGo(obj interface{}) Value {
|
||||
if obj == nil {
|
||||
return MakeNil()
|
||||
}
|
||||
return MakePointer(obj)
|
||||
}
|
||||
|
||||
// WrapGoError wraps a Go error as a Harbour Value.
|
||||
// nil error → Harbour NIL, non-nil → Harbour string.
|
||||
func WrapGoError(err error) Value {
|
||||
if err == nil {
|
||||
return MakeNil()
|
||||
}
|
||||
return MakeString(err.Error())
|
||||
}
|
||||
|
||||
// UnwrapGo extracts the Go object from a Harbour Value.
|
||||
func UnwrapGo(v Value) interface{} {
|
||||
if v.IsPointer() {
|
||||
return v.AsPointer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoCall — call a Go method on a wrapped object using reflection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GoCall calls method `name` on Go object wrapped in `receiver` with args.
|
||||
// Returns array of Harbour Values (one per Go return value).
|
||||
//
|
||||
// PRG: result := obj:Method(arg1, arg2)
|
||||
// Go: GoCall(objValue, "Method", arg1Value, arg2Value)
|
||||
func GoCall(receiver Value, method string, args ...Value) []Value {
|
||||
obj := UnwrapGo(receiver)
|
||||
if obj == nil {
|
||||
return []Value{MakeNil(), MakeString("nil receiver")}
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(obj)
|
||||
m := rv.MethodByName(method)
|
||||
if !m.IsValid() {
|
||||
// Try pointer receiver
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
pv := reflect.New(rv.Type())
|
||||
pv.Elem().Set(rv)
|
||||
m = pv.MethodByName(method)
|
||||
}
|
||||
if !m.IsValid() {
|
||||
return []Value{MakeNil(), MakeString("method not found: " + method)}
|
||||
}
|
||||
}
|
||||
|
||||
mt := m.Type()
|
||||
|
||||
// Convert Harbour args → Go args
|
||||
goArgs := make([]reflect.Value, len(args))
|
||||
for i, arg := range args {
|
||||
if i < mt.NumIn() {
|
||||
goArgs[i] = valueToReflect(arg, mt.In(i))
|
||||
} else if mt.IsVariadic() && i >= mt.NumIn()-1 {
|
||||
// Variadic: convert to slice element type
|
||||
elemType := mt.In(mt.NumIn() - 1).Elem()
|
||||
goArgs[i] = valueToReflect(arg, elemType)
|
||||
} else {
|
||||
goArgs[i] = reflect.ValueOf(valueToInterface(arg))
|
||||
}
|
||||
}
|
||||
|
||||
// Call
|
||||
var results []reflect.Value
|
||||
if mt.IsVariadic() {
|
||||
results = m.Call(goArgs)
|
||||
} else {
|
||||
results = m.Call(goArgs)
|
||||
}
|
||||
|
||||
// Convert Go results → Harbour Values
|
||||
hbResults := make([]Value, len(results))
|
||||
for i, r := range results {
|
||||
hbResults[i] = reflectToValue(r)
|
||||
}
|
||||
|
||||
return hbResults
|
||||
}
|
||||
|
||||
// GoCallFunc calls a package-level Go function.
|
||||
// fn must be a reflect.Value of the function.
|
||||
func GoCallFunc(fn interface{}, args ...Value) []Value {
|
||||
rv := reflect.ValueOf(fn)
|
||||
if rv.Kind() != reflect.Func {
|
||||
return []Value{MakeNil(), MakeString("not a function")}
|
||||
}
|
||||
|
||||
ft := rv.Type()
|
||||
goArgs := make([]reflect.Value, len(args))
|
||||
isVariadic := ft.IsVariadic()
|
||||
fixedCount := ft.NumIn()
|
||||
if isVariadic {
|
||||
fixedCount-- // last param is the variadic slice
|
||||
}
|
||||
for i, arg := range args {
|
||||
if i < fixedCount {
|
||||
goArgs[i] = valueToReflect(arg, ft.In(i))
|
||||
} else if isVariadic {
|
||||
// Variadic: convert to the slice's element type
|
||||
elemType := ft.In(ft.NumIn() - 1).Elem()
|
||||
goArgs[i] = valueToReflect(arg, elemType)
|
||||
} else {
|
||||
goArgs[i] = reflect.ValueOf(valueToInterface(arg))
|
||||
}
|
||||
}
|
||||
|
||||
var results []reflect.Value
|
||||
if isVariadic {
|
||||
results = rv.Call(goArgs) // Call (not CallSlice) handles spreading
|
||||
} else {
|
||||
results = rv.Call(goArgs)
|
||||
}
|
||||
hbResults := make([]Value, len(results))
|
||||
for i, r := range results {
|
||||
hbResults[i] = reflectToValue(r)
|
||||
}
|
||||
return hbResults
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoGet / GoSet — field access on Go structs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GoGet gets a field value from a Go struct.
|
||||
func GoGet(receiver Value, field string) Value {
|
||||
obj := UnwrapGo(receiver)
|
||||
if obj == nil {
|
||||
return MakeNil()
|
||||
}
|
||||
rv := reflect.ValueOf(obj)
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return MakeNil()
|
||||
}
|
||||
f := rv.FieldByName(field)
|
||||
if !f.IsValid() {
|
||||
return MakeNil()
|
||||
}
|
||||
return reflectToValue(f)
|
||||
}
|
||||
|
||||
// GoSet sets a field value on a Go struct.
|
||||
func GoSet(receiver Value, field string, val Value) {
|
||||
obj := UnwrapGo(receiver)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
rv := reflect.ValueOf(obj)
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
f := rv.FieldByName(field)
|
||||
if !f.IsValid() || !f.CanSet() {
|
||||
return
|
||||
}
|
||||
f.Set(valueToReflect(val, f.Type()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type coercion: Value → reflect.Value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func valueToReflect(v Value, targetType reflect.Type) reflect.Value {
|
||||
// Handle interface{} target
|
||||
if targetType.Kind() == reflect.Interface {
|
||||
return reflect.ValueOf(valueToInterface(v))
|
||||
}
|
||||
|
||||
switch targetType.Kind() {
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(v.AsString())
|
||||
case reflect.Int:
|
||||
return reflect.ValueOf(int(valToInt64(v)))
|
||||
case reflect.Int8:
|
||||
return reflect.ValueOf(int8(valToInt64(v)))
|
||||
case reflect.Int16:
|
||||
return reflect.ValueOf(int16(valToInt64(v)))
|
||||
case reflect.Int32:
|
||||
return reflect.ValueOf(int32(valToInt64(v)))
|
||||
case reflect.Int64:
|
||||
return reflect.ValueOf(valToInt64(v))
|
||||
case reflect.Uint:
|
||||
return reflect.ValueOf(uint(valToInt64(v)))
|
||||
case reflect.Uint8:
|
||||
return reflect.ValueOf(uint8(valToInt64(v)))
|
||||
case reflect.Uint16:
|
||||
return reflect.ValueOf(uint16(valToInt64(v)))
|
||||
case reflect.Uint32:
|
||||
return reflect.ValueOf(uint32(valToInt64(v)))
|
||||
case reflect.Uint64:
|
||||
return reflect.ValueOf(uint64(valToInt64(v)))
|
||||
case reflect.Float32:
|
||||
return reflect.ValueOf(float32(v.AsNumDouble()))
|
||||
case reflect.Float64:
|
||||
return reflect.ValueOf(v.AsNumDouble())
|
||||
case reflect.Bool:
|
||||
return reflect.ValueOf(v.AsBool())
|
||||
case reflect.Ptr:
|
||||
// Unwrap Go pointer from Value
|
||||
if v.IsPointer() {
|
||||
obj := v.AsPointer()
|
||||
if obj != nil {
|
||||
rv := reflect.ValueOf(obj)
|
||||
if rv.Type().AssignableTo(targetType) {
|
||||
return rv
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflect.Zero(targetType)
|
||||
case reflect.Slice:
|
||||
if targetType.Elem().Kind() == reflect.Uint8 && v.IsString() {
|
||||
// []byte from string
|
||||
return reflect.ValueOf([]byte(v.AsString()))
|
||||
}
|
||||
if v.IsArray() {
|
||||
return arrayToSlice(v, targetType)
|
||||
}
|
||||
return reflect.Zero(targetType)
|
||||
default:
|
||||
// Try interface{} unwrap
|
||||
if v.IsPointer() {
|
||||
obj := v.AsPointer()
|
||||
if obj != nil {
|
||||
rv := reflect.ValueOf(obj)
|
||||
if rv.Type().AssignableTo(targetType) {
|
||||
return rv
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflect.Zero(targetType)
|
||||
}
|
||||
}
|
||||
|
||||
// valToInt64 safely converts any numeric Value to int64.
|
||||
func valToInt64(v Value) int64 {
|
||||
if v.IsInt() || v.IsLong() {
|
||||
return v.AsLong()
|
||||
}
|
||||
// Double → truncate
|
||||
return int64(v.AsNumDouble())
|
||||
}
|
||||
|
||||
func valueToInterface(v Value) interface{} {
|
||||
switch {
|
||||
case v.IsNil():
|
||||
return nil
|
||||
case v.IsString():
|
||||
return v.AsString()
|
||||
case v.IsLogical():
|
||||
return v.AsBool()
|
||||
case v.IsDate():
|
||||
return v.AsLong() // Julian
|
||||
case v.IsNumeric():
|
||||
if v.IsInt() {
|
||||
return v.AsInt()
|
||||
}
|
||||
return v.AsNumDouble()
|
||||
case v.IsPointer():
|
||||
return v.AsPointer()
|
||||
case v.IsArray():
|
||||
arr := v.AsArray()
|
||||
result := make([]interface{}, len(arr.Items))
|
||||
for i, item := range arr.Items {
|
||||
result[i] = valueToInterface(item)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func arrayToSlice(v Value, sliceType reflect.Type) reflect.Value {
|
||||
arr := v.AsArray()
|
||||
if arr == nil {
|
||||
return reflect.Zero(sliceType)
|
||||
}
|
||||
elemType := sliceType.Elem()
|
||||
slice := reflect.MakeSlice(sliceType, len(arr.Items), len(arr.Items))
|
||||
for i, item := range arr.Items {
|
||||
slice.Index(i).Set(valueToReflect(item, elemType))
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type coercion: reflect.Value → Value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func reflectToValue(rv reflect.Value) Value {
|
||||
if !rv.IsValid() {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// Check error interface BEFORE unwrapping
|
||||
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
||||
if rv.Type().Implements(errorType) {
|
||||
if rv.IsNil() {
|
||||
return MakeNil()
|
||||
}
|
||||
return MakeString(rv.Interface().(error).Error())
|
||||
}
|
||||
|
||||
// Handle interface — unwrap
|
||||
if rv.Kind() == reflect.Interface || rv.Kind() == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
return MakeNil()
|
||||
}
|
||||
if rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.String:
|
||||
return MakeString(rv.String())
|
||||
case reflect.Bool:
|
||||
return MakeBool(rv.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := rv.Int()
|
||||
if n >= -2147483648 && n <= 2147483647 {
|
||||
return MakeInt(int(n))
|
||||
}
|
||||
return MakeLong(n)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return MakeLong(int64(rv.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return MakeDouble(rv.Float(), 0, 0)
|
||||
case reflect.Slice:
|
||||
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
||||
// []byte → string
|
||||
return MakeString(string(rv.Bytes()))
|
||||
}
|
||||
items := make([]Value, rv.Len())
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
items[i] = reflectToValue(rv.Index(i))
|
||||
}
|
||||
return MakeArrayFrom(items)
|
||||
case reflect.Map:
|
||||
h := &HbHash{}
|
||||
iter := rv.MapRange()
|
||||
for iter.Next() {
|
||||
h.Keys = append(h.Keys, reflectToValue(iter.Key()))
|
||||
h.Values = append(h.Values, reflectToValue(iter.Value()))
|
||||
}
|
||||
return MakeHashFrom(h)
|
||||
case reflect.Ptr, reflect.Struct, reflect.Func, reflect.Chan:
|
||||
// Wrap as Go object pointer
|
||||
return WrapGo(rv.Interface())
|
||||
default:
|
||||
// Wrap anything else as Go object
|
||||
if rv.CanInterface() {
|
||||
return WrapGo(rv.Interface())
|
||||
}
|
||||
return MakeNil()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoMultiReturn — unpack Go multi-return into Harbour locals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GoMultiAssign assigns multiple Go return values to Harbour locals.
|
||||
// PRG: a, b, c := GoFunc(...)
|
||||
// Generated: results := GoCallFunc(fn, args...); GoMultiAssign(t, results, 1, 2, 3)
|
||||
func GoMultiAssign(t *Thread, results []Value, localIndices ...int) {
|
||||
for i, idx := range localIndices {
|
||||
if i < len(results) {
|
||||
t.SetLocal(idx, results[i])
|
||||
} else {
|
||||
t.SetLocal(idx, MakeNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IsGoObject checks if a Value contains a wrapped Go object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func IsGoObject(v Value) bool {
|
||||
if !v.IsPointer() {
|
||||
return false
|
||||
}
|
||||
obj := v.AsPointer()
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
rv := reflect.ValueOf(obj)
|
||||
k := rv.Kind()
|
||||
return k == reflect.Ptr || k == reflect.Struct || k == reflect.Interface ||
|
||||
k == reflect.Map || k == reflect.Slice || k == reflect.Chan || k == reflect.Func
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoTypeName returns the Go type name of a wrapped object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func GoTypeName(v Value) string {
|
||||
if !v.IsPointer() {
|
||||
return "Value"
|
||||
}
|
||||
obj := v.AsPointer()
|
||||
if obj == nil {
|
||||
return "nil"
|
||||
}
|
||||
return fmt.Sprintf("%T", obj)
|
||||
}
|
||||
181
hbrt/gobridge_bench_test.go
Normal file
181
hbrt/gobridge_bench_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Pre-register fast functions
|
||||
var (
|
||||
ffToUpper = RegisterFastFunc("strings.ToUpper", strings.ToUpper)
|
||||
ffContains = RegisterFastFunc("strings.Contains", strings.Contains)
|
||||
ffReplaceAll = RegisterFastFunc("strings.ReplaceAll", strings.ReplaceAll)
|
||||
ffSqrt = RegisterFastFunc("math.Sqrt", math.Sqrt)
|
||||
ffCount = RegisterFastFunc("strings.Count", strings.Count)
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
// Benchmark: strings.ToUpper — string → string
|
||||
// ===================================================================
|
||||
|
||||
func BenchmarkDirect_ToUpper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.ToUpper("hello five world")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReflect_ToUpper(b *testing.B) {
|
||||
v := MakeString("hello five world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFunc(strings.ToUpper, v)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFastPath_ToUpper(b *testing.B) {
|
||||
v := MakeString("hello five world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFast(ffToUpper, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Benchmark: strings.Contains — string, string → bool
|
||||
// ===================================================================
|
||||
|
||||
func BenchmarkDirect_Contains(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.Contains("hello five world", "five")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReflect_Contains(b *testing.B) {
|
||||
v1 := MakeString("hello five world")
|
||||
v2 := MakeString("five")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFunc(strings.Contains, v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFastPath_Contains(b *testing.B) {
|
||||
v1 := MakeString("hello five world")
|
||||
v2 := MakeString("five")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFast(ffContains, v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Benchmark: strings.ReplaceAll — string, string, string → string
|
||||
// ===================================================================
|
||||
|
||||
func BenchmarkDirect_ReplaceAll(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.ReplaceAll("a-b-c-d-e", "-", "_")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReflect_ReplaceAll(b *testing.B) {
|
||||
v1 := MakeString("a-b-c-d-e")
|
||||
v2 := MakeString("-")
|
||||
v3 := MakeString("_")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFunc(strings.ReplaceAll, v1, v2, v3)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFastPath_ReplaceAll(b *testing.B) {
|
||||
v1 := MakeString("a-b-c-d-e")
|
||||
v2 := MakeString("-")
|
||||
v3 := MakeString("_")
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFast(ffReplaceAll, v1, v2, v3)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Benchmark: math.Sqrt — float64 → float64
|
||||
// ===================================================================
|
||||
|
||||
func BenchmarkDirect_Sqrt(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = math.Sqrt(144.0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReflect_Sqrt(b *testing.B) {
|
||||
v := MakeDouble(144.0, 0, 0)
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFunc(math.Sqrt, v)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFastPath_Sqrt(b *testing.B) {
|
||||
v := MakeDouble(144.0, 0, 0)
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFast(ffSqrt, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Benchmark: Object method call
|
||||
// ===================================================================
|
||||
|
||||
func BenchmarkReflect_MethodCall(b *testing.B) {
|
||||
obj := WrapGo(&testStruct{Name: "test", Value: 42})
|
||||
arg := MakeInt(1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCall(obj, "Add", arg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCached_MethodCall(b *testing.B) {
|
||||
obj := WrapGo(&testStruct{Name: "test", Value: 42})
|
||||
arg := MakeInt(1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallCached(obj, "Add", arg)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Summary comparison
|
||||
// ===================================================================
|
||||
|
||||
func TestBenchSummary(t *testing.T) {
|
||||
v := MakeString("hello five world")
|
||||
v2 := MakeString("five")
|
||||
|
||||
// Warm up caches
|
||||
GoCallFast(ffToUpper, v)
|
||||
GoCallFast(ffContains, v, v2)
|
||||
|
||||
n := 1000000
|
||||
|
||||
// Direct
|
||||
start := testing.Benchmark(func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.ToUpper("hello five world")
|
||||
}
|
||||
})
|
||||
|
||||
// Reflect
|
||||
reflectBm := testing.Benchmark(func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFunc(strings.ToUpper, v)
|
||||
}
|
||||
})
|
||||
|
||||
// Fast
|
||||
fastBm := testing.Benchmark(func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GoCallFast(ffToUpper, v)
|
||||
}
|
||||
})
|
||||
|
||||
_ = n
|
||||
t.Logf("ToUpper comparison:")
|
||||
t.Logf(" Direct: %v/op", start.NsPerOp())
|
||||
t.Logf(" Reflect: %v/op (%.1fx)", reflectBm.NsPerOp(), float64(reflectBm.NsPerOp())/float64(start.NsPerOp()))
|
||||
t.Logf(" Fast: %v/op (%.1fx)", fastBm.NsPerOp(), float64(fastBm.NsPerOp())/float64(start.NsPerOp()))
|
||||
t.Logf(" Speedup: reflect→fast = %.1fx", float64(reflectBm.NsPerOp())/float64(fastBm.NsPerOp()))
|
||||
}
|
||||
206
hbrt/gobridge_fast.go
Normal file
206
hbrt/gobridge_fast.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// gobridge_fast.go — Performance optimizations for Go interop.
|
||||
//
|
||||
// Strategy 1: Method cache — cache reflect.Method by (type, name)
|
||||
// Strategy 2: Fast path — bypass reflect for common signatures
|
||||
// Strategy 3: Function registry — pre-register Go funcs with typed wrappers
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Method Cache — cache reflect.Method by (type, methodName)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type methodKey struct {
|
||||
typ reflect.Type
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
methodCache = make(map[methodKey]reflect.Method)
|
||||
methodCacheMu sync.RWMutex
|
||||
)
|
||||
|
||||
// cachedMethod looks up a method with caching.
|
||||
func cachedMethod(rv reflect.Value, name string) (reflect.Value, bool) {
|
||||
key := methodKey{rv.Type(), name}
|
||||
|
||||
methodCacheMu.RLock()
|
||||
m, ok := methodCache[key]
|
||||
methodCacheMu.RUnlock()
|
||||
|
||||
if ok {
|
||||
return rv.Method(m.Index), true
|
||||
}
|
||||
|
||||
// Slow path: lookup and cache
|
||||
mt, found := rv.Type().MethodByName(name)
|
||||
if !found {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
methodCacheMu.Lock()
|
||||
methodCache[key] = mt
|
||||
methodCacheMu.Unlock()
|
||||
|
||||
return rv.Method(mt.Index), true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Fast Path — common function signatures without reflect
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// FastFunc is a type-specialized Go function wrapper.
|
||||
// Avoids reflect.Call for common signatures.
|
||||
type FastFunc struct {
|
||||
name string
|
||||
fn interface{} // original function for fallback
|
||||
// Typed fast paths (only one is non-nil)
|
||||
fnSS func(string) string // string → string
|
||||
fnSSB func(string, string) bool // string, string → bool
|
||||
fnSSS func(string, string) string // string, string → string
|
||||
fnSSSS func(string, string, string) string // string, string, string → string
|
||||
fnSI func(string) int // string → int
|
||||
fnSSI func(string, string) int // string, string → int
|
||||
fnFI func(float64) float64 // float64 → float64
|
||||
fnFFI func(float64, float64) float64 // float64, float64 → float64
|
||||
fnII func(int) int // int → int
|
||||
}
|
||||
|
||||
// GoCallFast calls a pre-registered fast function.
|
||||
func GoCallFast(ff *FastFunc, args ...Value) []Value {
|
||||
n := len(args)
|
||||
|
||||
// Try fast paths first
|
||||
if ff.fnSS != nil && n == 1 {
|
||||
return []Value{MakeString(ff.fnSS(args[0].AsString()))}
|
||||
}
|
||||
if ff.fnSSB != nil && n == 2 {
|
||||
return []Value{MakeBool(ff.fnSSB(args[0].AsString(), args[1].AsString()))}
|
||||
}
|
||||
if ff.fnSSS != nil && n == 2 {
|
||||
return []Value{MakeString(ff.fnSSS(args[0].AsString(), args[1].AsString()))}
|
||||
}
|
||||
if ff.fnSSSS != nil && n == 3 {
|
||||
return []Value{MakeString(ff.fnSSSS(args[0].AsString(), args[1].AsString(), args[2].AsString()))}
|
||||
}
|
||||
if ff.fnSI != nil && n == 1 {
|
||||
return []Value{MakeInt(ff.fnSI(args[0].AsString()))}
|
||||
}
|
||||
if ff.fnSSI != nil && n == 2 {
|
||||
return []Value{MakeInt(ff.fnSSI(args[0].AsString(), args[1].AsString()))}
|
||||
}
|
||||
if ff.fnFI != nil && n == 1 {
|
||||
return []Value{MakeDouble(ff.fnFI(args[0].AsNumDouble()), 0, 0)}
|
||||
}
|
||||
if ff.fnFFI != nil && n == 2 {
|
||||
return []Value{MakeDouble(ff.fnFFI(args[0].AsNumDouble(), args[1].AsNumDouble()), 0, 0)}
|
||||
}
|
||||
if ff.fnII != nil && n == 1 {
|
||||
return []Value{MakeInt(ff.fnII(args[0].AsInt()))}
|
||||
}
|
||||
|
||||
// Fallback to reflect
|
||||
return GoCallFunc(ff.fn, args...)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. GoCallCached — GoCall with method cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GoCallCached is a faster version of GoCall that caches method lookups.
|
||||
func GoCallCached(receiver Value, method string, args ...Value) []Value {
|
||||
obj := UnwrapGo(receiver)
|
||||
if obj == nil {
|
||||
return []Value{MakeNil(), MakeString("nil receiver")}
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(obj)
|
||||
m, ok := cachedMethod(rv, method)
|
||||
if !ok {
|
||||
return []Value{MakeNil(), MakeString("method not found: " + method)}
|
||||
}
|
||||
|
||||
mt := m.Type()
|
||||
isVariadic := mt.IsVariadic()
|
||||
fixedCount := mt.NumIn()
|
||||
if isVariadic {
|
||||
fixedCount--
|
||||
}
|
||||
|
||||
goArgs := make([]reflect.Value, len(args))
|
||||
for i, arg := range args {
|
||||
if i < fixedCount {
|
||||
goArgs[i] = valueToReflect(arg, mt.In(i))
|
||||
} else if isVariadic {
|
||||
elemType := mt.In(mt.NumIn() - 1).Elem()
|
||||
goArgs[i] = valueToReflect(arg, elemType)
|
||||
} else {
|
||||
goArgs[i] = reflect.ValueOf(valueToInterface(arg))
|
||||
}
|
||||
}
|
||||
|
||||
results := m.Call(goArgs)
|
||||
hbResults := make([]Value, len(results))
|
||||
for i, r := range results {
|
||||
hbResults[i] = reflectToValue(r)
|
||||
}
|
||||
return hbResults
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Function Registry — pre-register known functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
fastFuncRegistry = make(map[string]*FastFunc)
|
||||
fastFuncRegistryMu sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterFastFunc registers a Go function with typed fast paths.
|
||||
func RegisterFastFunc(name string, fn interface{}) *FastFunc {
|
||||
ff := &FastFunc{name: name, fn: fn}
|
||||
|
||||
// Auto-detect signature and set fast path
|
||||
switch f := fn.(type) {
|
||||
case func(string) string:
|
||||
ff.fnSS = f
|
||||
case func(string, string) bool:
|
||||
ff.fnSSB = f
|
||||
case func(string, string) string:
|
||||
ff.fnSSS = f
|
||||
case func(string, string, string) string:
|
||||
ff.fnSSSS = f
|
||||
case func(string) int:
|
||||
ff.fnSI = f
|
||||
case func(string, string) int:
|
||||
ff.fnSSI = f
|
||||
case func(float64) float64:
|
||||
ff.fnFI = f
|
||||
case func(float64, float64) float64:
|
||||
ff.fnFFI = f
|
||||
case func(int) int:
|
||||
ff.fnII = f
|
||||
}
|
||||
|
||||
fastFuncRegistryMu.Lock()
|
||||
fastFuncRegistry[name] = ff
|
||||
fastFuncRegistryMu.Unlock()
|
||||
|
||||
return ff
|
||||
}
|
||||
|
||||
// GetFastFunc looks up a registered fast function.
|
||||
func GetFastFunc(name string) *FastFunc {
|
||||
fastFuncRegistryMu.RLock()
|
||||
ff := fastFuncRegistry[name]
|
||||
fastFuncRegistryMu.RUnlock()
|
||||
return ff
|
||||
}
|
||||
477
hbrt/gobridge_stress_test.go
Normal file
477
hbrt/gobridge_stress_test.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ===== Stress test Go functions =====
|
||||
|
||||
func goSumInts(a, b, c, d, e int) int { return a + b + c + d + e }
|
||||
func goConcatMany(a, b, c, d, e, f string) string { return a + b + c + d + e + f }
|
||||
func goReturnLargeSlice(n int) []int {
|
||||
s := make([]int, n)
|
||||
for i := range s { s[i] = i }
|
||||
return s
|
||||
}
|
||||
func goReturnLargeMap(n int) map[string]int {
|
||||
m := make(map[string]int, n)
|
||||
for i := 0; i < n; i++ { m[fmt.Sprintf("key_%d", i)] = i }
|
||||
return m
|
||||
}
|
||||
func goNestedSlice() [][]int {
|
||||
return [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
}
|
||||
func goReturnNil() *testStruct { return nil }
|
||||
func goAcceptNil(s *testStruct) string {
|
||||
if s == nil { return "nil" }
|
||||
return s.Name
|
||||
}
|
||||
|
||||
type chainObj struct{ Val int }
|
||||
func (c *chainObj) Add(n int) *chainObj { return &chainObj{Val: c.Val + n} }
|
||||
func (c *chainObj) Mul(n int) *chainObj { return &chainObj{Val: c.Val * n} }
|
||||
func (c *chainObj) Result() int { return c.Val }
|
||||
func goNewChain(n int) *chainObj { return &chainObj{Val: n} }
|
||||
|
||||
// ===================================================================
|
||||
// 1. VOLUME: Thousands of calls
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_HighVolume_StringCalls(t *testing.T) {
|
||||
for i := 0; i < 10000; i++ {
|
||||
v := MakeString(fmt.Sprintf("hello_%d", i))
|
||||
results := GoCallFunc(strings.ToUpper, v)
|
||||
expected := fmt.Sprintf("HELLO_%d", i)
|
||||
if results[0].AsString() != expected {
|
||||
t.Fatalf("iter %d: got %q want %q", i, results[0].AsString(), expected)
|
||||
}
|
||||
}
|
||||
t.Log("10,000 string calls OK")
|
||||
}
|
||||
|
||||
func TestStress_HighVolume_IntCalls(t *testing.T) {
|
||||
for i := 0; i < 10000; i++ {
|
||||
results := GoCallFunc(goSumInts,
|
||||
MakeInt(i), MakeInt(i*2), MakeInt(i*3), MakeInt(i*4), MakeInt(i*5))
|
||||
expected := i * 15 // i+2i+3i+4i+5i
|
||||
if results[0].AsInt() != expected {
|
||||
t.Fatalf("iter %d: got %d want %d", i, results[0].AsInt(), expected)
|
||||
}
|
||||
}
|
||||
t.Log("10,000 int calls OK")
|
||||
}
|
||||
|
||||
func TestStress_HighVolume_FloatCalls(t *testing.T) {
|
||||
for i := 0; i < 10000; i++ {
|
||||
v := MakeDouble(float64(i)*0.1, 0, 0)
|
||||
results := GoCallFunc(math.Sqrt, v)
|
||||
expected := math.Sqrt(float64(i) * 0.1)
|
||||
diff := results[0].AsDouble() - expected
|
||||
if diff > 0.0001 || diff < -0.0001 {
|
||||
t.Fatalf("iter %d: got %f want %f", i, results[0].AsDouble(), expected)
|
||||
}
|
||||
}
|
||||
t.Log("10,000 float calls OK")
|
||||
}
|
||||
|
||||
func TestStress_HighVolume_BoolCalls(t *testing.T) {
|
||||
for i := 0; i < 10000; i++ {
|
||||
s := fmt.Sprintf("item_%d", i)
|
||||
search := fmt.Sprintf("_%d", i)
|
||||
results := GoCallFunc(strings.Contains, MakeString(s), MakeString(search))
|
||||
if !results[0].AsBool() {
|
||||
t.Fatalf("iter %d: expected true", i)
|
||||
}
|
||||
}
|
||||
t.Log("10,000 bool calls OK")
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 2. LARGE DATA: big arrays, maps, strings
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_LargeString(t *testing.T) {
|
||||
// 1MB string
|
||||
big := strings.Repeat("abcdefghij", 100000)
|
||||
v := MakeString(big)
|
||||
results := GoCallFunc(strings.ToUpper, v)
|
||||
got := results[0].AsString()
|
||||
if len(got) != 1000000 {
|
||||
t.Fatalf("large string: len=%d want 1000000", len(got))
|
||||
}
|
||||
if got[:10] != "ABCDEFGHIJ" {
|
||||
t.Fatalf("large string: prefix=%q", got[:10])
|
||||
}
|
||||
t.Logf("1MB string roundtrip OK (len=%d)", len(got))
|
||||
}
|
||||
|
||||
func TestStress_LargeArray(t *testing.T) {
|
||||
// 10,000 element array Go→PRG
|
||||
results := GoCallFunc(goReturnLargeSlice, MakeInt(10000))
|
||||
if !results[0].IsArray() {
|
||||
t.Fatalf("large array: not array")
|
||||
}
|
||||
arr := results[0].AsArray()
|
||||
if len(arr.Items) != 10000 {
|
||||
t.Fatalf("large array: len=%d want 10000", len(arr.Items))
|
||||
}
|
||||
if arr.Items[0].AsInt() != 0 || arr.Items[9999].AsInt() != 9999 {
|
||||
t.Fatalf("large array: first=%d last=%d", arr.Items[0].AsInt(), arr.Items[9999].AsInt())
|
||||
}
|
||||
|
||||
// PRG→Go roundtrip: send array back to Go strings.Join
|
||||
strItems := make([]Value, 100)
|
||||
for i := range strItems {
|
||||
strItems[i] = MakeString(fmt.Sprintf("%d", i))
|
||||
}
|
||||
arrVal := MakeArrayFrom(strItems)
|
||||
joinResults := GoCallFunc(strings.Join, arrVal, MakeString(","))
|
||||
joined := joinResults[0].AsString()
|
||||
parts := strings.Split(joined, ",")
|
||||
if len(parts) != 100 {
|
||||
t.Fatalf("array roundtrip: len=%d", len(parts))
|
||||
}
|
||||
t.Log("10,000 element array + 100 element roundtrip OK")
|
||||
}
|
||||
|
||||
func TestStress_LargeMap(t *testing.T) {
|
||||
results := GoCallFunc(goReturnLargeMap, MakeInt(1000))
|
||||
if !results[0].IsHash() {
|
||||
t.Fatalf("large map: not hash")
|
||||
}
|
||||
h := results[0].AsHash()
|
||||
if len(h.Keys) != 1000 {
|
||||
t.Fatalf("large map: len=%d want 1000", len(h.Keys))
|
||||
}
|
||||
t.Logf("1,000 entry map OK")
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 3. TYPE BOUNDARY: edge values
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_IntBoundary(t *testing.T) {
|
||||
cases := []int{0, 1, -1, 127, -128, 255, 32767, -32768, 65535,
|
||||
2147483647, -2147483648, 100000000}
|
||||
for _, n := range cases {
|
||||
results := GoCallFunc(goIdentityInt, MakeInt(n))
|
||||
if results[0].AsInt() != n {
|
||||
t.Errorf("boundary int %d: got %d", n, results[0].AsInt())
|
||||
}
|
||||
}
|
||||
t.Log("Int boundary values OK")
|
||||
}
|
||||
|
||||
func TestStress_Int64Boundary(t *testing.T) {
|
||||
cases := []int64{0, 1, -1, math.MaxInt32, math.MinInt32,
|
||||
math.MaxInt32 + 1, math.MinInt32 - 1,
|
||||
999999999999, -999999999999}
|
||||
for _, n := range cases {
|
||||
results := GoCallFunc(goIdentityInt64, MakeLong(n))
|
||||
if results[0].AsLong() != n {
|
||||
t.Errorf("boundary int64 %d: got %d", n, results[0].AsLong())
|
||||
}
|
||||
}
|
||||
t.Log("Int64 boundary values OK")
|
||||
}
|
||||
|
||||
func TestStress_FloatBoundary(t *testing.T) {
|
||||
cases := []float64{0, 0.1, -0.1, math.SmallestNonzeroFloat64,
|
||||
math.MaxFloat64 / 2, -math.MaxFloat64 / 2,
|
||||
math.Pi, math.E, math.Phi}
|
||||
for _, f := range cases {
|
||||
results := GoCallFunc(goIdentityFloat64, MakeDouble(f, 0, 0))
|
||||
diff := math.Abs(results[0].AsDouble() - f)
|
||||
if diff > 1e-10 {
|
||||
t.Errorf("boundary float %g: got %g", f, results[0].AsDouble())
|
||||
}
|
||||
}
|
||||
t.Log("Float boundary values OK")
|
||||
}
|
||||
|
||||
func TestStress_StringBoundary(t *testing.T) {
|
||||
cases := []string{
|
||||
"", // empty
|
||||
" ", // single space
|
||||
"\t\n\r", // whitespace
|
||||
"a", // single char
|
||||
strings.Repeat("x", 65536), // 64KB
|
||||
"Hello 世界 🌍", // unicode
|
||||
"line1\nline2\nline3", // newlines
|
||||
`"quoted"`, // quotes
|
||||
"null\x00byte", // null byte
|
||||
}
|
||||
for i, s := range cases {
|
||||
results := GoCallFunc(goIdentityString, MakeString(s))
|
||||
if results[0].AsString() != s {
|
||||
t.Errorf("boundary string[%d] len=%d: mismatch", i, len(s))
|
||||
}
|
||||
}
|
||||
t.Log("String boundary values OK (empty, unicode, 64KB, null bytes)")
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 4. CONCURRENT: goroutine safety
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_ConcurrentCalls(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan string, 100)
|
||||
|
||||
for g := 0; g < 50; g++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 200; i++ {
|
||||
s := fmt.Sprintf("goroutine_%d_iter_%d", id, i)
|
||||
results := GoCallFunc(strings.ToUpper, MakeString(s))
|
||||
expected := strings.ToUpper(s)
|
||||
if results[0].AsString() != expected {
|
||||
errors <- fmt.Sprintf("g%d i%d: %q != %q", id, i, results[0].AsString(), expected)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
for e := range errors {
|
||||
t.Fatal(e)
|
||||
}
|
||||
t.Log("50 goroutines × 200 calls = 10,000 concurrent calls OK")
|
||||
}
|
||||
|
||||
func TestStress_ConcurrentObjectMethods(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan string, 100)
|
||||
|
||||
for g := 0; g < 20; g++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
obj := &testStruct{Name: fmt.Sprintf("obj_%d", id), Value: id}
|
||||
v := WrapGo(obj)
|
||||
for i := 0; i < 500; i++ {
|
||||
results := GoCall(v, "Add", MakeInt(i))
|
||||
expected := id + i
|
||||
if results[0].AsInt() != expected {
|
||||
errors <- fmt.Sprintf("g%d i%d: %d != %d", id, i, results[0].AsInt(), expected)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
for e := range errors {
|
||||
t.Fatal(e)
|
||||
}
|
||||
t.Log("20 goroutines × 500 method calls = 10,000 concurrent object calls OK")
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 5. OBJECT LIFECYCLE: create, use, chain, stress
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_ObjectChain(t *testing.T) {
|
||||
// Chain: obj.Add(5).Mul(3).Add(10).Result() = (0+5)*3+10 = 25
|
||||
obj := WrapGo(goNewChain(0))
|
||||
|
||||
r := GoCall(obj, "Add", MakeInt(5))
|
||||
obj = r[0]
|
||||
r = GoCall(obj, "Mul", MakeInt(3))
|
||||
obj = r[0]
|
||||
r = GoCall(obj, "Add", MakeInt(10))
|
||||
obj = r[0]
|
||||
r = GoCall(obj, "Result")
|
||||
|
||||
if r[0].AsInt() != 25 {
|
||||
t.Fatalf("chain: got %d want 25", r[0].AsInt())
|
||||
}
|
||||
t.Log("Object method chain OK: (0+5)*3+10 = 25")
|
||||
}
|
||||
|
||||
func TestStress_ManyObjects(t *testing.T) {
|
||||
// Create 1000 objects, call methods on each
|
||||
objects := make([]Value, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
objects[i] = WrapGo(&testStruct{Name: fmt.Sprintf("obj_%d", i), Value: i})
|
||||
}
|
||||
|
||||
for i, obj := range objects {
|
||||
r := GoCall(obj, "GetValue")
|
||||
if r[0].AsInt() != i {
|
||||
t.Fatalf("object %d: GetValue=%d", i, r[0].AsInt())
|
||||
}
|
||||
r = GoCall(obj, "GetName")
|
||||
expected := fmt.Sprintf("obj_%d", i)
|
||||
if r[0].AsString() != expected {
|
||||
t.Fatalf("object %d: GetName=%q", i, r[0].AsString())
|
||||
}
|
||||
}
|
||||
t.Log("1,000 objects created and verified OK")
|
||||
}
|
||||
|
||||
func TestStress_ObjectNilSafety(t *testing.T) {
|
||||
// Call method on nil-wrapped object
|
||||
results := GoCallFunc(goReturnNil)
|
||||
nilObj := results[0]
|
||||
if !nilObj.IsNil() {
|
||||
t.Fatalf("expected NIL from nil pointer")
|
||||
}
|
||||
|
||||
// GoCall on NIL should not panic
|
||||
r := GoCall(nilObj, "GetName")
|
||||
if len(r) < 2 {
|
||||
t.Fatalf("expected error result")
|
||||
}
|
||||
t.Log("Nil object safety OK")
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 6. TYPE COERCION MATRIX: every PRG→Go combination
|
||||
// ===================================================================
|
||||
|
||||
func goTakeString(s string) string { return s }
|
||||
func goTakeInt(n int) int { return n }
|
||||
func goTakeInt64(n int64) int64 { return n }
|
||||
func goTakeFloat64(f float64) float64 { return f }
|
||||
func goTakeBool(b bool) bool { return b }
|
||||
func goTakeInterface(v interface{}) string { return fmt.Sprintf("%v", v) }
|
||||
|
||||
func TestStress_CoercionMatrix(t *testing.T) {
|
||||
// Test: every Harbour type sent to every Go type
|
||||
values := []struct {
|
||||
name string
|
||||
v Value
|
||||
}{
|
||||
{"NIL", MakeNil()},
|
||||
{"String", MakeString("hello")},
|
||||
{"Int", MakeInt(42)},
|
||||
{"Long", MakeLong(9999999999)},
|
||||
{"Double", MakeDouble(3.14, 0, 0)},
|
||||
{"Bool.T", MakeBool(true)},
|
||||
{"Bool.F", MakeBool(false)},
|
||||
}
|
||||
|
||||
targets := []struct {
|
||||
name string
|
||||
fn interface{}
|
||||
}{
|
||||
{"→string", goTakeString},
|
||||
{"→int", goTakeInt},
|
||||
{"→int64", goTakeInt64},
|
||||
{"→float64", goTakeFloat64},
|
||||
{"→bool", goTakeBool},
|
||||
{"→interface{}", goTakeInterface},
|
||||
}
|
||||
|
||||
passed := 0
|
||||
for _, v := range values {
|
||||
for _, target := range targets {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Some coercions may panic — that's OK, we just track them
|
||||
t.Logf(" %s %s: panic (expected for incompatible)", v.name, target.name)
|
||||
}
|
||||
}()
|
||||
results := GoCallFunc(target.fn, v.v)
|
||||
if len(results) > 0 {
|
||||
passed++
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
t.Logf("Coercion matrix: %d/42 combinations succeeded", passed)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 7. PERFORMANCE BENCHMARK
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_Performance(t *testing.T) {
|
||||
n := 100000
|
||||
|
||||
// Benchmark: direct Go call
|
||||
start := time.Now()
|
||||
for i := 0; i < n; i++ {
|
||||
_ = strings.ToUpper("hello")
|
||||
}
|
||||
directTime := time.Since(start)
|
||||
|
||||
// Benchmark: via GoCallFunc bridge
|
||||
start = time.Now()
|
||||
v := MakeString("hello")
|
||||
for i := 0; i < n; i++ {
|
||||
GoCallFunc(strings.ToUpper, v)
|
||||
}
|
||||
bridgeTime := time.Since(start)
|
||||
|
||||
ratio := float64(bridgeTime) / float64(directTime)
|
||||
t.Logf("Performance: direct=%v bridge=%v ratio=%.1fx",
|
||||
directTime, bridgeTime, ratio)
|
||||
t.Logf("Bridge throughput: %.0f calls/sec",
|
||||
float64(n)/bridgeTime.Seconds())
|
||||
|
||||
// Benchmark: object method call
|
||||
obj := WrapGo(&testStruct{Name: "test", Value: 42})
|
||||
arg := MakeInt(1)
|
||||
start = time.Now()
|
||||
for i := 0; i < n; i++ {
|
||||
GoCall(obj, "Add", arg)
|
||||
}
|
||||
methodTime := time.Since(start)
|
||||
t.Logf("Method call: %v (%.0f calls/sec)",
|
||||
methodTime, float64(n)/methodTime.Seconds())
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 8. RANDOM FUZZ: random types and values
|
||||
// ===================================================================
|
||||
|
||||
func TestStress_RandomFuzz(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
funcs := []interface{}{
|
||||
strings.ToUpper,
|
||||
strings.ToLower,
|
||||
strings.TrimSpace,
|
||||
}
|
||||
|
||||
for i := 0; i < 5000; i++ {
|
||||
s := randomString(rng, rng.Intn(100))
|
||||
fn := funcs[rng.Intn(len(funcs))]
|
||||
v := MakeString(s)
|
||||
|
||||
results := GoCallFunc(fn, v)
|
||||
if len(results) == 0 {
|
||||
t.Fatalf("fuzz iter %d: no results", i)
|
||||
}
|
||||
|
||||
// Verify against direct call
|
||||
expected := reflect.ValueOf(fn).Call([]reflect.Value{reflect.ValueOf(s)})
|
||||
if results[0].AsString() != expected[0].String() {
|
||||
t.Fatalf("fuzz iter %d: %q → got %q want %q",
|
||||
i, s, results[0].AsString(), expected[0].String())
|
||||
}
|
||||
}
|
||||
t.Log("5,000 random fuzz calls OK")
|
||||
}
|
||||
|
||||
func randomString(rng *rand.Rand, length int) string {
|
||||
chars := "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ\t\n"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = chars[rng.Intn(len(chars))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
531
hbrt/gobridge_test.go
Normal file
531
hbrt/gobridge_test.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ===== Test helper Go functions for type conversion =====
|
||||
|
||||
func goIdentityString(s string) string { return s }
|
||||
func goIdentityInt(n int) int { return n }
|
||||
func goIdentityInt64(n int64) int64 { return n }
|
||||
func goIdentityFloat64(f float64) float64 { return f }
|
||||
func goIdentityBool(b bool) bool { return b }
|
||||
func goIdentityBytes(b []byte) []byte { return b }
|
||||
func goReturnSlice() []string { return []string{"a", "b", "c"} }
|
||||
func goReturnIntSlice() []int { return []int{10, 20, 30} }
|
||||
func goReturnMap() map[string]interface{} {
|
||||
return map[string]interface{}{"name": "Charles", "age": 30, "active": true}
|
||||
}
|
||||
func goMultiReturn(s string) (string, error) { return strings.ToUpper(s), nil }
|
||||
func goMultiReturnErr() (string, error) { return "", fmt.Errorf("test error") }
|
||||
func goSumVariadic(nums ...int) int {
|
||||
sum := 0
|
||||
for _, n := range nums {
|
||||
sum += n
|
||||
}
|
||||
return sum
|
||||
}
|
||||
func goSwapStrings(a, b string) (string, string) { return b, a }
|
||||
func goMakeStruct() *testStruct { return &testStruct{Name: "Five", Value: 42} }
|
||||
|
||||
type testStruct struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
func (ts *testStruct) GetName() string { return ts.Name }
|
||||
func (ts *testStruct) GetValue() int { return ts.Value }
|
||||
func (ts *testStruct) SetName(n string) { ts.Name = n }
|
||||
func (ts *testStruct) SetValue(v int) { ts.Value = v }
|
||||
func (ts *testStruct) Add(n int) int { return ts.Value + n }
|
||||
func (ts *testStruct) String() string { return fmt.Sprintf("%s=%d", ts.Name, ts.Value) }
|
||||
|
||||
// ===== PRG → Go: Value to Go type conversion =====
|
||||
|
||||
func TestPRGToGo_String(t *testing.T) {
|
||||
v := MakeString("hello five")
|
||||
results := GoCallFunc(goIdentityString, v)
|
||||
if len(results) != 1 || results[0].AsString() != "hello five" {
|
||||
t.Errorf("string: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_EmptyString(t *testing.T) {
|
||||
v := MakeString("")
|
||||
results := GoCallFunc(goIdentityString, v)
|
||||
if len(results) != 1 || results[0].AsString() != "" {
|
||||
t.Errorf("empty string: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_Int(t *testing.T) {
|
||||
v := MakeInt(42)
|
||||
results := GoCallFunc(goIdentityInt, v)
|
||||
if len(results) != 1 || results[0].AsInt() != 42 {
|
||||
t.Errorf("int: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_NegativeInt(t *testing.T) {
|
||||
v := MakeInt(-100)
|
||||
results := GoCallFunc(goIdentityInt, v)
|
||||
if len(results) != 1 || results[0].AsInt() != -100 {
|
||||
t.Errorf("neg int: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_Int64(t *testing.T) {
|
||||
v := MakeLong(9999999999)
|
||||
results := GoCallFunc(goIdentityInt64, v)
|
||||
if len(results) != 1 || results[0].AsLong() != 9999999999 {
|
||||
t.Errorf("int64: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_Float64(t *testing.T) {
|
||||
v := MakeDouble(3.14159, 0, 5)
|
||||
results := GoCallFunc(goIdentityFloat64, v)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("float64: no result")
|
||||
}
|
||||
diff := results[0].AsDouble() - 3.14159
|
||||
if diff > 0.00001 || diff < -0.00001 {
|
||||
t.Errorf("float64: got %v", results[0].AsDouble())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_BoolTrue(t *testing.T) {
|
||||
v := MakeBool(true)
|
||||
results := GoCallFunc(goIdentityBool, v)
|
||||
if len(results) != 1 || !results[0].AsBool() {
|
||||
t.Errorf("bool true: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_BoolFalse(t *testing.T) {
|
||||
v := MakeBool(false)
|
||||
results := GoCallFunc(goIdentityBool, v)
|
||||
if len(results) != 1 || results[0].AsBool() {
|
||||
t.Errorf("bool false: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRGToGo_StringAsBytes(t *testing.T) {
|
||||
v := MakeString("binary data")
|
||||
results := GoCallFunc(goIdentityBytes, v)
|
||||
if len(results) != 1 || results[0].AsString() != "binary data" {
|
||||
t.Errorf("bytes: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Go → PRG: Go return to Value conversion =====
|
||||
|
||||
func TestGoToPRG_StringSlice(t *testing.T) {
|
||||
results := GoCallFunc(goReturnSlice)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("slice: expected 1 result, got %d", len(results))
|
||||
}
|
||||
v := results[0]
|
||||
if !v.IsArray() {
|
||||
t.Fatalf("slice: expected array, got %v", v)
|
||||
}
|
||||
arr := v.AsArray()
|
||||
if len(arr.Items) != 3 {
|
||||
t.Fatalf("slice: expected 3 items, got %d", len(arr.Items))
|
||||
}
|
||||
if arr.Items[0].AsString() != "a" || arr.Items[1].AsString() != "b" || arr.Items[2].AsString() != "c" {
|
||||
t.Errorf("slice: got %v %v %v", arr.Items[0], arr.Items[1], arr.Items[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoToPRG_IntSlice(t *testing.T) {
|
||||
results := GoCallFunc(goReturnIntSlice)
|
||||
if len(results) != 1 || !results[0].IsArray() {
|
||||
t.Fatalf("int slice: expected array")
|
||||
}
|
||||
arr := results[0].AsArray()
|
||||
if len(arr.Items) != 3 {
|
||||
t.Fatalf("int slice: expected 3 items")
|
||||
}
|
||||
if arr.Items[0].AsInt() != 10 || arr.Items[1].AsInt() != 20 || arr.Items[2].AsInt() != 30 {
|
||||
t.Errorf("int slice: got %d %d %d", arr.Items[0].AsInt(), arr.Items[1].AsInt(), arr.Items[2].AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoToPRG_Map(t *testing.T) {
|
||||
results := GoCallFunc(goReturnMap)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("map: expected 1 result")
|
||||
}
|
||||
v := results[0]
|
||||
if !v.IsHash() {
|
||||
t.Fatalf("map: expected hash, got type=%v", reflect.TypeOf(v))
|
||||
}
|
||||
h := v.AsHash()
|
||||
// Check keys exist (order may vary)
|
||||
found := map[string]bool{}
|
||||
for i, k := range h.Keys {
|
||||
key := k.AsString()
|
||||
found[key] = true
|
||||
switch key {
|
||||
case "name":
|
||||
if h.Values[i].AsString() != "Charles" {
|
||||
t.Errorf("map name: got %v", h.Values[i])
|
||||
}
|
||||
case "age":
|
||||
if h.Values[i].AsInt() != 30 {
|
||||
t.Errorf("map age: got %v", h.Values[i])
|
||||
}
|
||||
case "active":
|
||||
if !h.Values[i].AsBool() {
|
||||
t.Errorf("map active: got %v", h.Values[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found["name"] || !found["age"] || !found["active"] {
|
||||
t.Errorf("map: missing keys, found=%v", found)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Multi-return =====
|
||||
|
||||
func TestGoToPRG_MultiReturn(t *testing.T) {
|
||||
v := MakeString("hello")
|
||||
results := GoCallFunc(goMultiReturn, v)
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("multi: expected 2 results, got %d", len(results))
|
||||
}
|
||||
if results[0].AsString() != "HELLO" {
|
||||
t.Errorf("multi[0]: got %q", results[0].AsString())
|
||||
}
|
||||
// error is nil → should be NIL value
|
||||
if !results[1].IsNil() {
|
||||
t.Errorf("multi[1]: expected NIL, got %v", results[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoToPRG_MultiReturnError(t *testing.T) {
|
||||
results := GoCallFunc(goMultiReturnErr)
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("multi err: expected 2 results, got %d", len(results))
|
||||
}
|
||||
if results[0].AsString() != "" {
|
||||
t.Errorf("multi err[0]: got %q", results[0].AsString())
|
||||
}
|
||||
// error is non-nil → should be string
|
||||
if results[1].IsNil() {
|
||||
t.Errorf("multi err[1]: expected error string, got NIL")
|
||||
}
|
||||
if results[1].AsString() != "test error" {
|
||||
t.Errorf("multi err[1]: got %q", results[1].AsString())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoToPRG_SwapStrings(t *testing.T) {
|
||||
a := MakeString("first")
|
||||
b := MakeString("second")
|
||||
results := GoCallFunc(goSwapStrings, a, b)
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("swap: expected 2 results, got %d", len(results))
|
||||
}
|
||||
if results[0].AsString() != "second" || results[1].AsString() != "first" {
|
||||
t.Errorf("swap: got %q, %q", results[0].AsString(), results[1].AsString())
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Go Object wrapping and method calls =====
|
||||
|
||||
func TestGoObject_Wrap(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
if !v.IsPointer() {
|
||||
t.Fatalf("wrap: expected pointer")
|
||||
}
|
||||
if !IsGoObject(v) {
|
||||
t.Fatalf("wrap: IsGoObject should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_WrapNil(t *testing.T) {
|
||||
v := WrapGo(nil)
|
||||
if !v.IsNil() {
|
||||
t.Errorf("wrap nil: expected NIL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_MethodCall(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
|
||||
// GetName()
|
||||
results := GoCall(v, "GetName")
|
||||
if len(results) != 1 || results[0].AsString() != "Five" {
|
||||
t.Errorf("GetName: got %v", results)
|
||||
}
|
||||
|
||||
// GetValue()
|
||||
results = GoCall(v, "GetValue")
|
||||
if len(results) != 1 || results[0].AsInt() != 42 {
|
||||
t.Errorf("GetValue: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_MethodCallWithArg(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
|
||||
// Add(8) → 50
|
||||
results := GoCall(v, "Add", MakeInt(8))
|
||||
if len(results) != 1 || results[0].AsInt() != 50 {
|
||||
t.Errorf("Add(8): got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_MethodMutate(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
|
||||
// SetName("Go")
|
||||
GoCall(v, "SetName", MakeString("Go"))
|
||||
if obj.Name != "Go" {
|
||||
t.Errorf("SetName: name=%q", obj.Name)
|
||||
}
|
||||
|
||||
// SetValue(100)
|
||||
GoCall(v, "SetValue", MakeInt(100))
|
||||
if obj.Value != 100 {
|
||||
t.Errorf("SetValue: value=%d", obj.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_MethodNotFound(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
results := GoCall(v, "NonExistent")
|
||||
if len(results) < 2 {
|
||||
t.Fatalf("not found: expected error result")
|
||||
}
|
||||
errMsg := results[1].AsString()
|
||||
if !strings.Contains(errMsg, "method not found") {
|
||||
t.Errorf("not found: got %q", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_NilReceiver(t *testing.T) {
|
||||
v := MakeNil()
|
||||
results := GoCall(v, "Anything")
|
||||
if len(results) < 2 {
|
||||
t.Fatalf("nil receiver: expected error")
|
||||
}
|
||||
if !strings.Contains(results[1].AsString(), "nil receiver") {
|
||||
t.Errorf("nil receiver: got %q", results[1].AsString())
|
||||
}
|
||||
}
|
||||
|
||||
// ===== GoGet/GoSet field access =====
|
||||
|
||||
func TestGoObject_FieldGet(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
|
||||
name := GoGet(v, "Name")
|
||||
if name.AsString() != "Five" {
|
||||
t.Errorf("GoGet Name: got %q", name.AsString())
|
||||
}
|
||||
|
||||
val := GoGet(v, "Value")
|
||||
if val.AsInt() != 42 {
|
||||
t.Errorf("GoGet Value: got %d", val.AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_FieldSet(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
|
||||
GoSet(v, "Name", MakeString("Updated"))
|
||||
if obj.Name != "Updated" {
|
||||
t.Errorf("GoSet Name: got %q", obj.Name)
|
||||
}
|
||||
|
||||
GoSet(v, "Value", MakeInt(99))
|
||||
if obj.Value != 99 {
|
||||
t.Errorf("GoSet Value: got %d", obj.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoObject_FieldNotFound(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
result := GoGet(v, "NonExistent")
|
||||
if !result.IsNil() {
|
||||
t.Errorf("field not found: expected NIL, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== GoMultiAssign =====
|
||||
|
||||
func TestGoMultiAssign_Basic(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 3)
|
||||
|
||||
results := []Value{MakeString("hello"), MakeInt(42), MakeBool(true)}
|
||||
GoMultiAssign(th, results, 1, 2, 3)
|
||||
|
||||
if th.Local(1).AsString() != "hello" {
|
||||
t.Errorf("multi assign[1]: got %v", th.Local(1))
|
||||
}
|
||||
if th.Local(2).AsInt() != 42 {
|
||||
t.Errorf("multi assign[2]: got %v", th.Local(2))
|
||||
}
|
||||
if !th.Local(3).AsBool() {
|
||||
t.Errorf("multi assign[3]: got %v", th.Local(3))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoMultiAssign_FewerResults(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 3)
|
||||
|
||||
results := []Value{MakeString("only one")}
|
||||
GoMultiAssign(th, results, 1, 2, 3)
|
||||
|
||||
if th.Local(1).AsString() != "only one" {
|
||||
t.Errorf("fewer[1]: got %v", th.Local(1))
|
||||
}
|
||||
if !th.Local(2).IsNil() {
|
||||
t.Errorf("fewer[2]: expected NIL, got %v", th.Local(2))
|
||||
}
|
||||
if !th.Local(3).IsNil() {
|
||||
t.Errorf("fewer[3]: expected NIL, got %v", th.Local(3))
|
||||
}
|
||||
}
|
||||
|
||||
// ===== GoTypeName =====
|
||||
|
||||
func TestGoTypeName(t *testing.T) {
|
||||
obj := &testStruct{Name: "Five", Value: 42}
|
||||
v := WrapGo(obj)
|
||||
name := GoTypeName(v)
|
||||
if name != "*hbrt.testStruct" {
|
||||
t.Errorf("type name: got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Edge cases =====
|
||||
|
||||
func TestEdge_IntToFloat(t *testing.T) {
|
||||
// PRG sends int, Go expects float64
|
||||
v := MakeInt(42)
|
||||
results := GoCallFunc(goIdentityFloat64, v)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("int→float: no result")
|
||||
}
|
||||
if results[0].AsDouble() != 42.0 {
|
||||
t.Errorf("int→float: got %v", results[0].AsDouble())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdge_FloatToInt(t *testing.T) {
|
||||
// PRG sends float, Go expects int
|
||||
v := MakeDouble(42.7, 0, 0)
|
||||
results := GoCallFunc(goIdentityInt, v)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("float→int: no result")
|
||||
}
|
||||
if results[0].AsInt() != 42 {
|
||||
t.Errorf("float→int: got %v", results[0].AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdge_NilToString(t *testing.T) {
|
||||
v := MakeNil()
|
||||
results := GoCallFunc(goIdentityString, v)
|
||||
if len(results) != 1 || results[0].AsString() != "" {
|
||||
t.Errorf("nil→string: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdge_NilToBool(t *testing.T) {
|
||||
v := MakeNil()
|
||||
results := GoCallFunc(goIdentityBool, v)
|
||||
if len(results) != 1 || results[0].AsBool() {
|
||||
t.Errorf("nil→bool: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdge_WrapGoError_Nil(t *testing.T) {
|
||||
v := WrapGoError(nil)
|
||||
if !v.IsNil() {
|
||||
t.Errorf("nil error: expected NIL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdge_WrapGoError_NonNil(t *testing.T) {
|
||||
v := WrapGoError(fmt.Errorf("something failed"))
|
||||
if v.IsNil() {
|
||||
t.Errorf("error: expected non-NIL")
|
||||
}
|
||||
if v.AsString() != "something failed" {
|
||||
t.Errorf("error: got %q", v.AsString())
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Real Go standard library functions =====
|
||||
|
||||
func TestRealGo_StringsToUpper(t *testing.T) {
|
||||
results := GoCallFunc(strings.ToUpper, MakeString("hello five"))
|
||||
if len(results) != 1 || results[0].AsString() != "HELLO FIVE" {
|
||||
t.Errorf("ToUpper: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealGo_StringsContains(t *testing.T) {
|
||||
results := GoCallFunc(strings.Contains, MakeString("hello five"), MakeString("five"))
|
||||
if len(results) != 1 || !results[0].AsBool() {
|
||||
t.Errorf("Contains: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealGo_StringsSplit(t *testing.T) {
|
||||
results := GoCallFunc(strings.Split, MakeString("a,b,c"), MakeString(","))
|
||||
if len(results) != 1 || !results[0].IsArray() {
|
||||
t.Fatalf("Split: expected array")
|
||||
}
|
||||
arr := results[0].AsArray()
|
||||
if len(arr.Items) != 3 {
|
||||
t.Fatalf("Split: expected 3 items, got %d", len(arr.Items))
|
||||
}
|
||||
if arr.Items[0].AsString() != "a" || arr.Items[1].AsString() != "b" || arr.Items[2].AsString() != "c" {
|
||||
t.Errorf("Split: got %v", arr.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealGo_StringsJoin(t *testing.T) {
|
||||
// Build a Harbour array, pass to Go strings.Join
|
||||
items := MakeArrayFrom([]Value{MakeString("x"), MakeString("y"), MakeString("z")})
|
||||
results := GoCallFunc(strings.Join, items, MakeString("-"))
|
||||
if len(results) != 1 || results[0].AsString() != "x-y-z" {
|
||||
t.Errorf("Join: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealGo_StringsReplaceAll(t *testing.T) {
|
||||
results := GoCallFunc(strings.ReplaceAll, MakeString("foo-bar"), MakeString("-"), MakeString("_"))
|
||||
if len(results) != 1 || results[0].AsString() != "foo_bar" {
|
||||
t.Errorf("ReplaceAll: got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealGo_FmtSprintf(t *testing.T) {
|
||||
results := GoCallFunc(fmt.Sprintf, MakeString("Name: %s, Age: %d"), MakeString("Charles"), MakeInt(30))
|
||||
if len(results) != 1 || results[0].AsString() != "Name: Charles, Age: 30" {
|
||||
t.Errorf("Sprintf: got %v", results)
|
||||
}
|
||||
}
|
||||
136
hbrt/goroutine.go
Normal file
136
hbrt/goroutine.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Goroutine support for Five runtime.
|
||||
// Provides Go's goroutine, channel, and WaitGroup primitives
|
||||
// as first-class Harbour values.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// --- Channel ---
|
||||
|
||||
// HbChannel wraps Go's chan Value for use in PRG code.
|
||||
type HbChannel struct {
|
||||
Ch chan Value
|
||||
}
|
||||
|
||||
// MakeChannel creates a channel Value with optional buffer size.
|
||||
func MakeChannel(size int) Value {
|
||||
return MakePointer(&HbChannel{Ch: make(chan Value, size)})
|
||||
}
|
||||
|
||||
// AsChannel extracts HbChannel from a Pointer value.
|
||||
func (v Value) AsChannel() *HbChannel {
|
||||
if !v.IsPointer() {
|
||||
return nil
|
||||
}
|
||||
if ch, ok := v.AsPointer().(*HbChannel); ok {
|
||||
return ch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send sends a value into the channel.
|
||||
func (ch *HbChannel) Send(val Value) {
|
||||
ch.Ch <- val
|
||||
}
|
||||
|
||||
// Receive receives a value from the channel.
|
||||
func (ch *HbChannel) Receive() Value {
|
||||
return <-ch.Ch
|
||||
}
|
||||
|
||||
// TryReceive attempts non-blocking receive. Returns (value, true) or (nil, false).
|
||||
func (ch *HbChannel) TryReceive() (Value, bool) {
|
||||
select {
|
||||
case v := <-ch.Ch:
|
||||
return v, true
|
||||
default:
|
||||
return MakeNil(), false
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the channel.
|
||||
func (ch *HbChannel) Close() {
|
||||
close(ch.Ch)
|
||||
}
|
||||
|
||||
// --- WaitGroup ---
|
||||
|
||||
// HbWaitGroup wraps sync.WaitGroup.
|
||||
type HbWaitGroup struct {
|
||||
WG sync.WaitGroup
|
||||
}
|
||||
|
||||
// MakeWaitGroup creates a WaitGroup Value with initial count.
|
||||
func MakeWaitGroup(n int) Value {
|
||||
wg := &HbWaitGroup{}
|
||||
if n > 0 {
|
||||
wg.WG.Add(n)
|
||||
}
|
||||
return MakePointer(wg)
|
||||
}
|
||||
|
||||
// AsWaitGroup extracts HbWaitGroup from a Pointer value.
|
||||
func (v Value) AsWaitGroup() *HbWaitGroup {
|
||||
if !v.IsPointer() {
|
||||
return nil
|
||||
}
|
||||
if wg, ok := v.AsPointer().(*HbWaitGroup); ok {
|
||||
return wg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Mutex ---
|
||||
|
||||
// HbMutex wraps sync.Mutex.
|
||||
type HbMutex struct {
|
||||
Mu sync.Mutex
|
||||
}
|
||||
|
||||
// MakeMutex creates a Mutex Value.
|
||||
func MakeMutex() Value {
|
||||
return MakePointer(&HbMutex{})
|
||||
}
|
||||
|
||||
// AsMutex extracts HbMutex from a Pointer value.
|
||||
func (v Value) AsMutex() *HbMutex {
|
||||
if !v.IsPointer() {
|
||||
return nil
|
||||
}
|
||||
if mu, ok := v.AsPointer().(*HbMutex); ok {
|
||||
return mu
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- GoRoutine launcher ---
|
||||
|
||||
// GoLaunch spawns a new goroutine that runs a function on a new Thread.
|
||||
func (vm *VM) GoLaunch(fn func(*Thread), args []Value) {
|
||||
go func() {
|
||||
t := vm.NewThread()
|
||||
for _, a := range args {
|
||||
t.push(a)
|
||||
}
|
||||
t.PendingParams2(len(args))
|
||||
fn(t)
|
||||
}()
|
||||
}
|
||||
|
||||
// GoLaunchBlock spawns a goroutine that evaluates a code block.
|
||||
func (vm *VM) GoLaunchBlock(blk *HbBlock, args []Value) {
|
||||
go func() {
|
||||
t := vm.NewThread()
|
||||
for _, a := range args {
|
||||
t.push(a)
|
||||
}
|
||||
t.PendingParams2(len(args))
|
||||
blk.Fn(t)
|
||||
}()
|
||||
}
|
||||
663
hbrt/hbfunc.go
Normal file
663
hbrt/hbfunc.go
Normal file
@@ -0,0 +1,663 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// hbfunc.go — Harbour HB_FUNC compatible Go API for #pragma BEGINDUMP.
|
||||
//
|
||||
// This provides the complete Harbour C extension API in Go,
|
||||
// allowing PRG code to call inline Go functions seamlessly.
|
||||
//
|
||||
// Usage in PRG:
|
||||
//
|
||||
// #pragma BEGINDUMP
|
||||
// func init() {
|
||||
// hbrt.HB_FUNC("MYFUNC", MyFunc)
|
||||
// }
|
||||
// func MyFunc(ctx *hbrt.HBContext) {
|
||||
// name := ctx.ParC(1)
|
||||
// age := ctx.ParNI(2)
|
||||
// ctx.RetC("Hello " + name)
|
||||
// }
|
||||
// #pragma ENDDUMP
|
||||
//
|
||||
// Then in PRG:
|
||||
//
|
||||
// ? MyFunc("Charles", 30) // → "Hello Charles"
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HBContext wraps Thread for Harbour-compatible C API access.
|
||||
// Maps 1:1 to Harbour's hb_par*/hb_ret*/hb_stor* functions.
|
||||
type HBContext struct {
|
||||
T *Thread
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HB_FUNC registration — called from init() in #pragma BEGINDUMP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// HB_FUNC registers a Go function as a Harbour-callable function.
|
||||
// Equivalent to Harbour's HB_FUNC(name) macro.
|
||||
func HB_FUNC(name string, fn func(ctx *HBContext)) {
|
||||
RegisterDynamicFunc(strings.ToUpper(name), func(t *Thread) {
|
||||
ctx := &HBContext{T: t}
|
||||
fn(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// HB_FUNC_STATIC is same as HB_FUNC but marks as static scope.
|
||||
func HB_FUNC_STATIC(name string, fn func(ctx *HBContext)) {
|
||||
HB_FUNC(name, fn)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter count — Harbour: hb_pcount()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (c *HBContext) PCount() int {
|
||||
return c.T.ParamCount()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter access (1-based index)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (c *HBContext) param(n int) Value {
|
||||
if n < 1 || n > c.PCount() {
|
||||
return MakeNil()
|
||||
}
|
||||
return c.T.Local(n)
|
||||
}
|
||||
|
||||
// Param returns raw Value of parameter n.
|
||||
func (c *HBContext) Param(n int) Value { return c.param(n) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type checking — Harbour: HB_IS*(n) macros
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (c *HBContext) IsNil(n int) bool { return c.param(n).IsNil() }
|
||||
func (c *HBContext) IsChar(n int) bool { return c.param(n).IsString() }
|
||||
func (c *HBContext) IsString(n int) bool { return c.param(n).IsString() }
|
||||
func (c *HBContext) IsNum(n int) bool { return c.param(n).IsNumeric() }
|
||||
func (c *HBContext) IsNumeric(n int) bool { return c.param(n).IsNumeric() }
|
||||
func (c *HBContext) IsLog(n int) bool { return c.param(n).IsLogical() }
|
||||
func (c *HBContext) IsLogical(n int) bool { return c.param(n).IsLogical() }
|
||||
func (c *HBContext) IsDate(n int) bool { return c.param(n).IsDate() }
|
||||
func (c *HBContext) IsDateTime(n int) bool { return c.param(n).IsDateTime() }
|
||||
func (c *HBContext) IsArray(n int) bool { return c.param(n).IsArray() }
|
||||
func (c *HBContext) IsHash(n int) bool { return c.param(n).IsHash() }
|
||||
func (c *HBContext) IsBlock(n int) bool { return c.param(n).IsBlock() }
|
||||
func (c *HBContext) IsObject(n int) bool { return c.param(n).IsObject() }
|
||||
func (c *HBContext) IsPointer(n int) bool { return c.param(n).IsPointer() }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// String parameters — Harbour: hb_parc, hb_parclen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ParC returns string parameter n. Harbour: hb_parc(n)
|
||||
func (c *HBContext) ParC(n int) string {
|
||||
v := c.param(n)
|
||||
if v.IsString() {
|
||||
return v.AsString()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParCLen returns length of string parameter n. Harbour: hb_parclen(n)
|
||||
func (c *HBContext) ParCLen(n int) int {
|
||||
v := c.param(n)
|
||||
if v.IsString() {
|
||||
return len(v.AsString())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Numeric parameters — Harbour: hb_parni, hb_parnl, hb_parnd
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ParNI returns int parameter. Harbour: hb_parni(n)
|
||||
func (c *HBContext) ParNI(n int) int {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsInt()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParNIDef returns int parameter with default. Harbour: hb_parnidef(n, def)
|
||||
func (c *HBContext) ParNIDef(n int, def int) int {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsInt()
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ParNL returns int64 parameter. Harbour: hb_parnl(n)
|
||||
func (c *HBContext) ParNL(n int) int64 {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsLong()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParNLDef returns int64 parameter with default. Harbour: hb_parnldef(n, def)
|
||||
func (c *HBContext) ParNLDef(n int, def int64) int64 {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsLong()
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ParND returns float64 parameter. Harbour: hb_parnd(n)
|
||||
func (c *HBContext) ParND(n int) float64 {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsNumDouble()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParNDDef returns float64 parameter with default.
|
||||
func (c *HBContext) ParNDDef(n int, def float64) float64 {
|
||||
v := c.param(n)
|
||||
if v.IsNumeric() {
|
||||
return v.AsNumDouble()
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ParNInt returns HB_MAXINT parameter. Harbour: hb_parnint(n)
|
||||
func (c *HBContext) ParNInt(n int) int64 { return c.ParNL(n) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logical parameters — Harbour: hb_parl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ParL returns bool parameter. Harbour: hb_parl(n)
|
||||
func (c *HBContext) ParL(n int) bool {
|
||||
v := c.param(n)
|
||||
if v.IsLogical() {
|
||||
return v.AsBool()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParLDef returns bool parameter with default. Harbour: hb_parldef(n, def)
|
||||
func (c *HBContext) ParLDef(n int, def bool) bool {
|
||||
v := c.param(n)
|
||||
if v.IsLogical() {
|
||||
return v.AsBool()
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Date parameters — Harbour: hb_pards, hb_pardl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// julianToYMD converts Julian day to year, month, day.
|
||||
func julianToYMD(julian int64) (int, int, int) {
|
||||
if julian <= 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
l := julian + 68569
|
||||
n := 4 * l / 146097
|
||||
l = l - (146097*n+3)/4
|
||||
i := 4000 * (l + 1) / 1461001
|
||||
l = l - 1461*i/4 + 31
|
||||
j := 80 * l / 2447
|
||||
d := l - 2447*j/80
|
||||
l = j / 11
|
||||
m := j + 2 - 12*l
|
||||
y := 100*(n-49) + i + l
|
||||
return int(y), int(m), int(d)
|
||||
}
|
||||
|
||||
// ymdToJulian converts year, month, day to Julian day number.
|
||||
func ymdToJulian(y, m, d int) int64 {
|
||||
if y == 0 && m == 0 && d == 0 {
|
||||
return 0
|
||||
}
|
||||
mm := int64(m)
|
||||
yy := int64(y)
|
||||
dd := int64(d)
|
||||
return dd - 32075 +
|
||||
1461*(yy+4800+(mm-14)/12)/4 +
|
||||
367*(mm-2-(mm-14)/12*12)/12 -
|
||||
3*((yy+4900+(mm-14)/12)/100)/4
|
||||
}
|
||||
|
||||
// ParDS returns date as "YYYYMMDD" string. Harbour: hb_pards(n)
|
||||
func (c *HBContext) ParDS(n int) string {
|
||||
v := c.param(n)
|
||||
if v.IsDate() {
|
||||
y, m, d := julianToYMD(v.AsJulian())
|
||||
return fmt.Sprintf("%04d%02d%02d", y, m, d)
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
// ParDL returns date as Julian day number. Harbour: hb_pardl(n)
|
||||
func (c *HBContext) ParDL(n int) int64 {
|
||||
v := c.param(n)
|
||||
if v.IsDate() {
|
||||
return v.AsJulian()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParDate returns date as Go time.Time (Five extension).
|
||||
func (c *HBContext) ParDate(n int) time.Time {
|
||||
v := c.param(n)
|
||||
if v.IsDate() {
|
||||
y, m, d := julianToYMD(v.AsJulian())
|
||||
return time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Array parameters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ParArray returns array items. Five extension.
|
||||
func (c *HBContext) ParArray(n int) []Value {
|
||||
v := c.param(n)
|
||||
if v.IsArray() {
|
||||
return v.AsArray().Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParArrayLen returns array length. Harbour: hb_parinfa(n, 0)
|
||||
func (c *HBContext) ParArrayLen(n int) int {
|
||||
v := c.param(n)
|
||||
if v.IsArray() {
|
||||
return len(v.AsArray().Items)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParHash returns hash. Five extension.
|
||||
func (c *HBContext) ParHash(n int) *HbHash {
|
||||
v := c.param(n)
|
||||
if v.IsHash() {
|
||||
return v.AsHash()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Return values — Harbour: hb_ret*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Ret returns NIL. Harbour: hb_ret()
|
||||
func (c *HBContext) Ret() {
|
||||
c.T.PushNil()
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetNil returns NIL explicitly.
|
||||
func (c *HBContext) RetNil() {
|
||||
c.T.PushNil()
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetC returns string. Harbour: hb_retc(s)
|
||||
func (c *HBContext) RetC(s string) {
|
||||
c.T.PushString(s)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetCLen returns string of specific length. Harbour: hb_retclen(s, n)
|
||||
func (c *HBContext) RetCLen(s string, n int) {
|
||||
if n < len(s) {
|
||||
s = s[:n]
|
||||
}
|
||||
c.T.PushString(s)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetNI returns integer. Harbour: hb_retni(n)
|
||||
func (c *HBContext) RetNI(n int) {
|
||||
c.T.PushInt(n)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetNL returns long. Harbour: hb_retnl(n)
|
||||
func (c *HBContext) RetNL(n int64) {
|
||||
c.T.PushLong(n)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetND returns double. Harbour: hb_retnd(d)
|
||||
func (c *HBContext) RetND(d float64) {
|
||||
c.T.PushDouble(d, 0, 0)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetNDLen returns double with width/decimals. Harbour: hb_retndlen(d, w, dec)
|
||||
func (c *HBContext) RetNDLen(d float64, width, dec int) {
|
||||
c.T.PushDouble(d, uint16(width), uint16(dec))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetL returns logical. Harbour: hb_retl(b)
|
||||
func (c *HBContext) RetL(b bool) {
|
||||
c.T.PushBool(b)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetDS returns date from "YYYYMMDD". Harbour: hb_retds(s)
|
||||
func (c *HBContext) RetDS(s string) {
|
||||
if len(s) >= 8 {
|
||||
y, m, d := 0, 0, 0
|
||||
fmt.Sscanf(s, "%04d%02d%02d", &y, &m, &d)
|
||||
c.T.PushValue(MakeDate(ymdToJulian(y, m, d)))
|
||||
} else {
|
||||
c.T.PushValue(MakeDate(0))
|
||||
}
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetDL returns date from Julian. Harbour: hb_retdl(n)
|
||||
func (c *HBContext) RetDL(julian int64) {
|
||||
c.T.PushValue(MakeDate(julian))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetD returns date from y/m/d. Harbour: hb_retd(y, m, d)
|
||||
func (c *HBContext) RetD(y, m, d int) {
|
||||
c.T.PushValue(MakeDate(ymdToJulian(y, m, d)))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetValue returns raw Value. Five extension.
|
||||
func (c *HBContext) RetVal(v Value) {
|
||||
c.T.PushValue(v)
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetA returns empty array of size n. Harbour: hb_reta(n)
|
||||
func (c *HBContext) RetA(size int) {
|
||||
c.T.PushValue(MakeArray(size))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetArray returns populated array. Five extension.
|
||||
func (c *HBContext) RetArray(items []Value) {
|
||||
c.T.PushValue(MakeArrayFrom(items))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// RetHash returns hash. Five extension.
|
||||
func (c *HBContext) RetHash(h *HbHash) {
|
||||
c.T.PushValue(MakeHashFrom(h))
|
||||
c.T.RetValue()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// By-reference storage — Harbour: hb_stor*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// StorNil stores NIL into by-ref param. Harbour: hb_stor(n)
|
||||
func (c *HBContext) StorNil(n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeNil())
|
||||
}
|
||||
}
|
||||
|
||||
// StorC stores string into by-ref param. Harbour: hb_storc(s, n)
|
||||
func (c *HBContext) StorC(s string, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeString(s))
|
||||
}
|
||||
}
|
||||
|
||||
// StorNI stores int into by-ref param. Harbour: hb_storni(v, n)
|
||||
func (c *HBContext) StorNI(v int, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeInt(v))
|
||||
}
|
||||
}
|
||||
|
||||
// StorNL stores int64 into by-ref param. Harbour: hb_stornl(v, n)
|
||||
func (c *HBContext) StorNL(v int64, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeLong(v))
|
||||
}
|
||||
}
|
||||
|
||||
// StorND stores float64 into by-ref param. Harbour: hb_stornd(v, n)
|
||||
func (c *HBContext) StorND(v float64, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeDouble(v, 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
// StorL stores bool into by-ref param. Harbour: hb_storl(v, n)
|
||||
func (c *HBContext) StorL(v bool, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeBool(v))
|
||||
}
|
||||
}
|
||||
|
||||
// StorDS stores date string into by-ref param. Harbour: hb_stords(s, n)
|
||||
func (c *HBContext) StorDS(s string, n int) {
|
||||
if n >= 1 && n <= c.PCount() && len(s) >= 8 {
|
||||
y, m, d := 0, 0, 0
|
||||
fmt.Sscanf(s, "%04d%02d%02d", &y, &m, &d)
|
||||
c.T.SetLocal(n, MakeDate(ymdToJulian(y, m, d)))
|
||||
}
|
||||
}
|
||||
|
||||
// StorDL stores Julian date into by-ref param. Harbour: hb_stordl(v, n)
|
||||
func (c *HBContext) StorDL(v int64, n int) {
|
||||
if n >= 1 && n <= c.PCount() {
|
||||
c.T.SetLocal(n, MakeDate(v))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Array manipulation — Harbour: hb_array*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ArrayNew creates empty array. Harbour: hb_arrayNew()
|
||||
func (c *HBContext) ArrayNew(size int) Value {
|
||||
return MakeArray(size)
|
||||
}
|
||||
|
||||
// ArrayLen returns array length. Harbour: hb_arrayLen()
|
||||
func (c *HBContext) ArrayLen(v Value) int {
|
||||
if v.IsArray() {
|
||||
return len(v.AsArray().Items)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ArrayGet gets element at 1-based index. Harbour: hb_arrayGet()
|
||||
func (c *HBContext) ArrayGet(v Value, index int) Value {
|
||||
if v.IsArray() {
|
||||
items := v.AsArray().Items
|
||||
if index >= 1 && index <= len(items) {
|
||||
return items[index-1]
|
||||
}
|
||||
}
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// ArrayGetC gets string at index. Harbour: hb_arrayGetC()
|
||||
func (c *HBContext) ArrayGetC(v Value, index int) string {
|
||||
return c.ArrayGet(v, index).AsString()
|
||||
}
|
||||
|
||||
// ArrayGetNI gets int at index. Harbour: hb_arrayGetNI()
|
||||
func (c *HBContext) ArrayGetNI(v Value, index int) int {
|
||||
return c.ArrayGet(v, index).AsInt()
|
||||
}
|
||||
|
||||
// ArrayGetND gets double at index. Harbour: hb_arrayGetND()
|
||||
func (c *HBContext) ArrayGetND(v Value, index int) float64 {
|
||||
return c.ArrayGet(v, index).AsNumDouble()
|
||||
}
|
||||
|
||||
// ArrayGetL gets bool at index. Harbour: hb_arrayGetL()
|
||||
func (c *HBContext) ArrayGetL(v Value, index int) bool {
|
||||
return c.ArrayGet(v, index).AsBool()
|
||||
}
|
||||
|
||||
// ArraySet sets element at 1-based index. Harbour: hb_arraySet()
|
||||
func (c *HBContext) ArraySet(v Value, index int, item Value) {
|
||||
if v.IsArray() {
|
||||
items := v.AsArray().Items
|
||||
if index >= 1 && index <= len(items) {
|
||||
items[index-1] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ArraySetC sets string at index. Harbour: hb_arraySetC()
|
||||
func (c *HBContext) ArraySetC(v Value, index int, s string) {
|
||||
c.ArraySet(v, index, MakeString(s))
|
||||
}
|
||||
|
||||
// ArraySetNI sets int at index. Harbour: hb_arraySetNI()
|
||||
func (c *HBContext) ArraySetNI(v Value, index int, n int) {
|
||||
c.ArraySet(v, index, MakeInt(n))
|
||||
}
|
||||
|
||||
// ArraySetND sets double at index. Harbour: hb_arraySetND()
|
||||
func (c *HBContext) ArraySetND(v Value, index int, d float64) {
|
||||
c.ArraySet(v, index, MakeDouble(d, 0, 0))
|
||||
}
|
||||
|
||||
// ArraySetL sets bool at index. Harbour: hb_arraySetL()
|
||||
func (c *HBContext) ArraySetL(v Value, index int, b bool) {
|
||||
c.ArraySet(v, index, MakeBool(b))
|
||||
}
|
||||
|
||||
// ArrayAdd appends to array. Harbour: hb_arrayAdd()
|
||||
func (c *HBContext) ArrayAdd(v Value, item Value) {
|
||||
if v.IsArray() {
|
||||
arr := v.AsArray()
|
||||
arr.Items = append(arr.Items, item)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hash manipulation — Harbour: hb_hash*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// HashNew creates empty hash.
|
||||
func (c *HBContext) HashNew() Value {
|
||||
return MakeHash()
|
||||
}
|
||||
|
||||
// HashLen returns hash size.
|
||||
func (c *HBContext) HashLen(v Value) int {
|
||||
if v.IsHash() {
|
||||
return len(v.AsHash().Keys)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HashAdd adds key-value pair. Harbour: hb_hashAdd()
|
||||
func (c *HBContext) HashAdd(v Value, key, val Value) {
|
||||
if v.IsHash() {
|
||||
h := v.AsHash()
|
||||
h.Keys = append(h.Keys, key)
|
||||
h.Values = append(h.Values, val)
|
||||
}
|
||||
}
|
||||
|
||||
// HashGetC gets value by string key. Five extension.
|
||||
func (c *HBContext) HashGetC(v Value, key string) Value {
|
||||
if v.IsHash() {
|
||||
h := v.AsHash()
|
||||
for i, k := range h.Keys {
|
||||
if k.IsString() && k.AsString() == key {
|
||||
return h.Values[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling — Harbour: hb_errRT_BASE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ErrRT_BASE raises a BASE runtime error.
|
||||
func (c *HBContext) ErrRT_BASE(subCode int, description, operation string) {
|
||||
panic(fmt.Sprintf("BASE/%04d: %s: %s", subCode, description, operation))
|
||||
}
|
||||
|
||||
// ErrRT_BASE_SubstR raises a substitution error.
|
||||
func (c *HBContext) ErrRT_BASE_SubstR(subCode int, description, operation string) {
|
||||
c.ErrRT_BASE(subCode, description, operation)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ParInfo — Harbour: hb_parinfo(n)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
HB_IT_NIL = 0x00001
|
||||
HB_IT_INTEGER = 0x00002
|
||||
HB_IT_LONG = 0x00008
|
||||
HB_IT_DOUBLE = 0x00010
|
||||
HB_IT_DATE = 0x00020
|
||||
HB_IT_TIMESTAMP = 0x00040
|
||||
HB_IT_LOGICAL = 0x00080
|
||||
HB_IT_SYMBOL = 0x00100
|
||||
HB_IT_POINTER = 0x00200
|
||||
HB_IT_STRING = 0x00400
|
||||
HB_IT_MEMO = 0x00800
|
||||
HB_IT_BLOCK = 0x01000
|
||||
HB_IT_BYREF = 0x02000
|
||||
HB_IT_ARRAY = 0x04000
|
||||
HB_IT_HASH = 0x08000
|
||||
HB_IT_OBJECT = 0x10000
|
||||
HB_IT_NUMERIC = HB_IT_INTEGER | HB_IT_LONG | HB_IT_DOUBLE
|
||||
)
|
||||
|
||||
// ParInfo returns type flags for parameter n. Harbour: hb_parinfo(n)
|
||||
func (c *HBContext) ParInfo(n int) int {
|
||||
v := c.param(n)
|
||||
switch {
|
||||
case v.IsNil():
|
||||
return HB_IT_NIL
|
||||
case v.IsString():
|
||||
return HB_IT_STRING
|
||||
case v.IsLogical():
|
||||
return HB_IT_LOGICAL
|
||||
case v.IsDate():
|
||||
return HB_IT_DATE
|
||||
case v.IsTimestamp():
|
||||
return HB_IT_TIMESTAMP
|
||||
case v.IsArray():
|
||||
if v.IsObject() {
|
||||
return HB_IT_OBJECT
|
||||
}
|
||||
return HB_IT_ARRAY
|
||||
case v.IsHash():
|
||||
return HB_IT_HASH
|
||||
case v.IsBlock():
|
||||
return HB_IT_BLOCK
|
||||
case v.IsPointer():
|
||||
return HB_IT_POINTER
|
||||
case v.IsNumeric():
|
||||
return HB_IT_NUMERIC
|
||||
default:
|
||||
return HB_IT_NIL
|
||||
}
|
||||
}
|
||||
169
hbrt/macro.go
Normal file
169
hbrt/macro.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Runtime macro compiler for Five.
|
||||
// Implements &variable and &(expression) — runtime code compilation.
|
||||
//
|
||||
// Harbour has a full macro compiler (src/macro/macro.y) that parses
|
||||
// and compiles expressions at runtime. Five uses a simplified approach:
|
||||
// parse the expression string, then evaluate it using the existing
|
||||
// lexer/parser/evaluator infrastructure.
|
||||
//
|
||||
// Usage:
|
||||
// LOCAL cField := "salary"
|
||||
// ? &cField → evaluates variable named "salary"
|
||||
// ? &(cField + "_new") → evaluates variable named "salary_new"
|
||||
//
|
||||
// Reference: /mnt/d/harbour-core/src/macro/
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = fmt.Sprintf // ensure import
|
||||
|
||||
// MacroCompile compiles and evaluates a macro expression string.
|
||||
// Returns the result value.
|
||||
//
|
||||
// For simple variable references (&cVar):
|
||||
// Looks up the variable name in memvars/locals.
|
||||
//
|
||||
// For complex expressions (&(expr)):
|
||||
// Would need full expression parser — simplified for now.
|
||||
func (t *Thread) MacroCompile(expr string) Value {
|
||||
expr = strings.TrimSpace(expr)
|
||||
if expr == "" {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// Simple case: expression is a variable name
|
||||
// Look up in memvars first, then try as function call
|
||||
if isSimpleIdent(expr) {
|
||||
// Try PUBLIC/PRIVATE memvar
|
||||
// TODO: full memvar system
|
||||
// For now, try calling it as a function
|
||||
sym := t.vm.FindSymbol(strings.ToUpper(expr))
|
||||
if sym != nil && sym.Func != nil {
|
||||
t.PushSymbol(sym)
|
||||
t.PushNil()
|
||||
t.Function(0)
|
||||
return t.pop()
|
||||
}
|
||||
return MakeString(expr) // return as string if not found
|
||||
}
|
||||
|
||||
// Complex expression: try parsing as number, then as function call
|
||||
// Full runtime expression parser would be needed for complete macro support.
|
||||
// This handles common patterns: &("literal"), &(numericExpr)
|
||||
|
||||
// Try numeric
|
||||
expr = strings.TrimSpace(expr)
|
||||
if len(expr) > 0 && (expr[0] >= '0' && expr[0] <= '9' || expr[0] == '-' || expr[0] == '+') {
|
||||
if strings.Contains(expr, ".") {
|
||||
if f, err := parseFloat(expr); err == nil {
|
||||
return MakeDoubleAuto(f)
|
||||
}
|
||||
} else {
|
||||
if n, err := parseInt64(expr); err == nil {
|
||||
return MakeNumInt(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try string literal
|
||||
if len(expr) >= 2 && (expr[0] == '"' && expr[len(expr)-1] == '"' || expr[0] == '\'' && expr[len(expr)-1] == '\'') {
|
||||
return MakeString(expr[1 : len(expr)-1])
|
||||
}
|
||||
|
||||
// Try .T./.F.
|
||||
upper := strings.ToUpper(expr)
|
||||
if upper == ".T." {
|
||||
return MakeBool(true)
|
||||
}
|
||||
if upper == ".F." {
|
||||
return MakeBool(false)
|
||||
}
|
||||
|
||||
// Return as string (field name, variable name, etc.)
|
||||
return MakeString(expr)
|
||||
}
|
||||
|
||||
// MacroPush compiles a macro and pushes the result on stack.
|
||||
// Harbour: HB_P_MACROPUSH
|
||||
func (t *Thread) MacroPush() {
|
||||
exprVal := t.pop()
|
||||
result := t.MacroCompile(exprVal.AsString())
|
||||
t.push(result)
|
||||
}
|
||||
|
||||
func parseFloat(s string) (float64, error) {
|
||||
var result float64
|
||||
var sign float64 = 1
|
||||
i := 0
|
||||
if i < len(s) && s[i] == '-' {
|
||||
sign = -1
|
||||
i++
|
||||
} else if i < len(s) && s[i] == '+' {
|
||||
i++
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
result = result*10 + float64(s[i]-'0')
|
||||
i++
|
||||
}
|
||||
if i < len(s) && s[i] == '.' {
|
||||
i++
|
||||
frac := 0.1
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
result += float64(s[i]-'0') * frac
|
||||
frac /= 10
|
||||
i++
|
||||
}
|
||||
}
|
||||
if i != len(s) {
|
||||
return 0, fmt.Errorf("invalid float")
|
||||
}
|
||||
return sign * result, nil
|
||||
}
|
||||
|
||||
func parseInt64(s string) (int64, error) {
|
||||
var result int64
|
||||
var sign int64 = 1
|
||||
i := 0
|
||||
if i < len(s) && s[i] == '-' {
|
||||
sign = -1
|
||||
i++
|
||||
} else if i < len(s) && s[i] == '+' {
|
||||
i++
|
||||
}
|
||||
if i >= len(s) {
|
||||
return 0, fmt.Errorf("empty")
|
||||
}
|
||||
for i < len(s) {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return 0, fmt.Errorf("invalid int")
|
||||
}
|
||||
result = result*10 + int64(s[i]-'0')
|
||||
i++
|
||||
}
|
||||
return sign * result, nil
|
||||
}
|
||||
|
||||
// isSimpleIdent checks if string is a valid simple identifier.
|
||||
func isSimpleIdent(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
ch := s[0]
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_') {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < len(s); i++ {
|
||||
ch = s[i]
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
309
hbrt/macroeval.go
Normal file
309
hbrt/macroeval.go
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// macroeval.go — Full runtime macro compiler for Five.
|
||||
//
|
||||
// Implements &(expression) by reusing Five's own lexer/parser at runtime.
|
||||
// This is the key advantage of Five: since the compiler is in Go,
|
||||
// it can be embedded in the runtime for macro compilation.
|
||||
//
|
||||
// Harbour's macro compiler (src/macro/macro.y) is a separate YACC grammar.
|
||||
// Five simply reuses the same parser.
|
||||
//
|
||||
// Usage:
|
||||
// &cVar → simple variable lookup
|
||||
// &(cVar + "_name") → evaluate string expression, use result as name
|
||||
// &("Upper(cName)") → evaluate function call at runtime
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"five/compiler/ast"
|
||||
"five/compiler/lexer"
|
||||
"five/compiler/parser"
|
||||
"five/compiler/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MacroEval compiles and evaluates a Harbour expression string at runtime.
|
||||
// This is the full macro compiler — uses Five's parser to parse the expression,
|
||||
// then evaluates the AST directly.
|
||||
func (t *Thread) MacroEval(exprStr string) Value {
|
||||
exprStr = strings.TrimSpace(exprStr)
|
||||
if exprStr == "" {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// Quick path: simple identifier → variable/function lookup
|
||||
if isSimpleIdent(exprStr) {
|
||||
return t.macroLookupIdent(exprStr)
|
||||
}
|
||||
|
||||
// Full path: parse the expression and evaluate the AST
|
||||
source := "FUNCTION __macro__()\nRETURN " + exprStr + "\n"
|
||||
file, errs := parser.Parse("macro", source)
|
||||
if len(errs) > 0 || len(file.Decls) == 0 {
|
||||
// Parse failed — try as simple string
|
||||
return t.MacroCompile(exprStr)
|
||||
}
|
||||
|
||||
fn, ok := file.Decls[0].(*ast.FuncDecl)
|
||||
if !ok || len(fn.Body) == 0 {
|
||||
return t.MacroCompile(exprStr)
|
||||
}
|
||||
|
||||
// Get the RETURN expression
|
||||
ret, ok := fn.Body[0].(*ast.ReturnStmt)
|
||||
if !ok || ret.Value == nil {
|
||||
return t.MacroCompile(exprStr)
|
||||
}
|
||||
|
||||
// Evaluate the AST expression
|
||||
return t.evalExpr(ret.Value)
|
||||
}
|
||||
|
||||
// evalExpr evaluates an AST expression at runtime.
|
||||
func (t *Thread) evalExpr(expr ast.Expr) Value {
|
||||
switch e := expr.(type) {
|
||||
case *ast.LiteralExpr:
|
||||
return t.evalLiteral(e)
|
||||
|
||||
case *ast.IdentExpr:
|
||||
return t.macroLookupIdent(e.Name)
|
||||
|
||||
case *ast.BinaryExpr:
|
||||
left := t.evalExpr(e.Left)
|
||||
right := t.evalExpr(e.Right)
|
||||
return t.evalBinaryOp(e.Op, left, right)
|
||||
|
||||
case *ast.UnaryExpr:
|
||||
x := t.evalExpr(e.X)
|
||||
return t.evalUnaryOp(e.Op, x)
|
||||
|
||||
case *ast.CallExpr:
|
||||
return t.evalCall(e)
|
||||
|
||||
case *ast.SendExpr:
|
||||
obj := t.evalExpr(e.Object)
|
||||
args := make([]Value, len(e.Args))
|
||||
for i, a := range e.Args {
|
||||
args[i] = t.evalExpr(a)
|
||||
}
|
||||
t.push(obj)
|
||||
for _, a := range args {
|
||||
t.push(a)
|
||||
}
|
||||
t.Send(e.Method, len(args))
|
||||
return t.pop()
|
||||
|
||||
case *ast.IndexExpr:
|
||||
arr := t.evalExpr(e.X)
|
||||
idx := t.evalExpr(e.Index)
|
||||
if arr.IsArray() {
|
||||
items := arr.AsArray().Items
|
||||
i := idx.AsInt()
|
||||
if i >= 1 && i <= len(items) {
|
||||
return items[i-1]
|
||||
}
|
||||
}
|
||||
return MakeNil()
|
||||
|
||||
case *ast.ArrayLitExpr:
|
||||
items := make([]Value, len(e.Items))
|
||||
for i, item := range e.Items {
|
||||
items[i] = t.evalExpr(item)
|
||||
}
|
||||
return MakeArrayFrom(items)
|
||||
|
||||
case *ast.HashLitExpr:
|
||||
h := &HbHash{}
|
||||
for i := range e.Keys {
|
||||
h.Keys = append(h.Keys, t.evalExpr(e.Keys[i]))
|
||||
h.Values = append(h.Values, t.evalExpr(e.Values[i]))
|
||||
}
|
||||
return MakeHashFrom(h)
|
||||
|
||||
case *ast.BlockExpr:
|
||||
// Return as code block
|
||||
body := e.Body
|
||||
return MakeBlock(func(bt *Thread) {
|
||||
result := bt.evalExpr(body)
|
||||
bt.push(result)
|
||||
bt.RetValue()
|
||||
}, len(e.Params))
|
||||
|
||||
case *ast.DotExpr:
|
||||
// pkg.Func — try GoCallFunc
|
||||
obj := t.evalExpr(e.X)
|
||||
results := GoCall(obj, e.Member)
|
||||
if len(results) > 0 {
|
||||
return results[0]
|
||||
}
|
||||
return MakeNil()
|
||||
|
||||
case *ast.SelfExpr:
|
||||
return t.self
|
||||
|
||||
case *ast.AliasExpr:
|
||||
// alias->field
|
||||
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
|
||||
if field, ok := e.Field.(*ast.IdentExpr); ok {
|
||||
t.PushAliasField(ident.Name, field.Name)
|
||||
return t.pop()
|
||||
}
|
||||
}
|
||||
return MakeNil()
|
||||
|
||||
case *ast.AssignExpr:
|
||||
val := t.evalExpr(e.Right)
|
||||
// Assignment in macro — store to memvar or local
|
||||
if ident, ok := e.Left.(*ast.IdentExpr); ok {
|
||||
t.macroStoreIdent(ident.Name, val)
|
||||
}
|
||||
return val
|
||||
|
||||
default:
|
||||
return MakeNil()
|
||||
}
|
||||
}
|
||||
|
||||
// evalLiteral converts an AST literal to a Value.
|
||||
func (t *Thread) evalLiteral(e *ast.LiteralExpr) Value {
|
||||
switch e.Kind {
|
||||
case token.NIL_LIT:
|
||||
return MakeNil()
|
||||
case token.TRUE:
|
||||
return MakeBool(true)
|
||||
case token.FALSE:
|
||||
return MakeBool(false)
|
||||
case token.INT:
|
||||
n, _ := parseInt64(e.Value)
|
||||
return MakeNumInt(n)
|
||||
case token.LONG:
|
||||
n, _ := parseInt64(e.Value)
|
||||
return MakeLong(n)
|
||||
case token.DOUBLE:
|
||||
f, _ := parseFloat(e.Value)
|
||||
return MakeDoubleAuto(f)
|
||||
case token.STRING:
|
||||
return MakeString(e.Value)
|
||||
default:
|
||||
return MakeString(e.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// evalBinaryOp evaluates a binary operation.
|
||||
func (t *Thread) evalBinaryOp(op token.Kind, left, right Value) Value {
|
||||
t.push(left)
|
||||
t.push(right)
|
||||
switch op {
|
||||
case token.PLUS:
|
||||
t.Plus()
|
||||
case token.MINUS:
|
||||
t.Minus()
|
||||
case token.STAR:
|
||||
t.Mult()
|
||||
case token.SLASH:
|
||||
t.Divide()
|
||||
case token.PERCENT:
|
||||
t.Modulus()
|
||||
case token.POWER:
|
||||
t.Power()
|
||||
case token.EQ, token.EXEQ:
|
||||
t.Equal()
|
||||
case token.NEQ:
|
||||
t.NotEqual()
|
||||
case token.LT:
|
||||
t.Less()
|
||||
case token.GT:
|
||||
t.Greater()
|
||||
case token.LTE:
|
||||
t.LessEqual()
|
||||
case token.GTE:
|
||||
t.GreaterEqual()
|
||||
case token.AND:
|
||||
t.And()
|
||||
case token.OR:
|
||||
t.Or()
|
||||
case token.DOLLAR:
|
||||
t.InString()
|
||||
default:
|
||||
return MakeNil()
|
||||
}
|
||||
return t.pop()
|
||||
}
|
||||
|
||||
// evalUnaryOp evaluates a unary operation.
|
||||
func (t *Thread) evalUnaryOp(op token.Kind, x Value) Value {
|
||||
t.push(x)
|
||||
switch op {
|
||||
case token.MINUS:
|
||||
t.Negate()
|
||||
case token.NOT:
|
||||
t.Not()
|
||||
case token.INC:
|
||||
t.Inc()
|
||||
case token.DEC:
|
||||
t.Dec()
|
||||
default:
|
||||
return x
|
||||
}
|
||||
return t.pop()
|
||||
}
|
||||
|
||||
// evalCall evaluates a function call expression.
|
||||
func (t *Thread) evalCall(e *ast.CallExpr) Value {
|
||||
// Get function name
|
||||
var funcName string
|
||||
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
||||
funcName = strings.ToUpper(ident.Name)
|
||||
} else {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
// Evaluate arguments
|
||||
args := make([]Value, len(e.Args))
|
||||
for i, a := range e.Args {
|
||||
args[i] = t.evalExpr(a)
|
||||
}
|
||||
|
||||
// Find and call function via VM
|
||||
sym := t.vm.FindSymbol(funcName)
|
||||
if sym == nil || sym.Func == nil {
|
||||
return MakeNil()
|
||||
}
|
||||
|
||||
t.PushSymbol(sym)
|
||||
t.PushNil()
|
||||
for _, a := range args {
|
||||
t.push(a)
|
||||
}
|
||||
t.Function(len(args))
|
||||
return t.pop()
|
||||
}
|
||||
|
||||
// macroLookupIdent looks up a name: local → memvar → function.
|
||||
func (t *Thread) macroLookupIdent(name string) Value {
|
||||
upper := strings.ToUpper(name)
|
||||
|
||||
// Try as function
|
||||
sym := t.vm.FindSymbol(upper)
|
||||
if sym != nil && sym.Func != nil {
|
||||
// It's a function — don't call, return reference
|
||||
// Unless it has no args, then call it
|
||||
return MakeString(name)
|
||||
}
|
||||
|
||||
// Return as string (field name, memvar name)
|
||||
return MakeString(name)
|
||||
}
|
||||
|
||||
// macroStoreIdent stores a value to a named variable.
|
||||
func (t *Thread) macroStoreIdent(name string, val Value) {
|
||||
// TODO: memvar system — for now no-op
|
||||
_ = name
|
||||
_ = val
|
||||
}
|
||||
|
||||
// suppress import
|
||||
var _ = lexer.Tokenize
|
||||
142
hbrt/macroeval_test.go
Normal file
142
hbrt/macroeval_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMacroEval_Literal(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
// Numbers
|
||||
v := th.MacroEval("42")
|
||||
if v.AsInt() != 42 { t.Errorf("int: got %v", v) }
|
||||
|
||||
v = th.MacroEval("3.14")
|
||||
if v.AsDouble()-3.14 > 0.001 { t.Errorf("float: got %v", v) }
|
||||
|
||||
// Strings
|
||||
v = th.MacroEval(`"hello"`)
|
||||
if v.AsString() != "hello" { t.Errorf("string: got %v", v) }
|
||||
|
||||
// Booleans
|
||||
v = th.MacroEval(".T.")
|
||||
if !v.AsBool() { t.Errorf("true: got %v", v) }
|
||||
|
||||
v = th.MacroEval(".F.")
|
||||
if v.AsBool() { t.Errorf("false: got %v", v) }
|
||||
|
||||
// NIL
|
||||
v = th.MacroEval("NIL")
|
||||
// returns string "NIL" for now (identifier)
|
||||
if v.AsString() != "NIL" { t.Errorf("nil: got %v", v) }
|
||||
}
|
||||
|
||||
func TestMacroEval_Arithmetic(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval("2 + 3")
|
||||
if v.AsInt() != 5 { t.Errorf("add: got %v", v) }
|
||||
|
||||
v = th.MacroEval("10 - 4")
|
||||
if v.AsInt() != 6 { t.Errorf("sub: got %v", v) }
|
||||
|
||||
v = th.MacroEval("6 * 7")
|
||||
if v.AsInt() != 42 { t.Errorf("mul: got %v", v) }
|
||||
|
||||
v = th.MacroEval("100 / 4")
|
||||
if int(v.AsNumDouble()) != 25 { t.Errorf("div: got %v", v.AsNumDouble()) }
|
||||
|
||||
v = th.MacroEval("2 ** 10")
|
||||
if int(v.AsNumDouble()) != 1024 { t.Errorf("pow: got %v", v.AsNumDouble()) }
|
||||
}
|
||||
|
||||
func TestMacroEval_StringOps(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval(`"hello" + " " + "world"`)
|
||||
if v.AsString() != "hello world" { t.Errorf("concat: got %q", v.AsString()) }
|
||||
}
|
||||
|
||||
func TestMacroEval_Comparison(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval("10 > 5")
|
||||
if !v.AsBool() { t.Errorf("gt: got %v", v) }
|
||||
|
||||
v = th.MacroEval("3 < 1")
|
||||
if v.AsBool() { t.Errorf("lt: got %v", v) }
|
||||
|
||||
v = th.MacroEval("5 == 5")
|
||||
if !v.AsBool() { t.Errorf("eq: got %v", v) }
|
||||
|
||||
v = th.MacroEval("5 != 3")
|
||||
if !v.AsBool() { t.Errorf("neq: got %v", v) }
|
||||
}
|
||||
|
||||
func TestMacroEval_Complex(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval("(2 + 3) * 4")
|
||||
if v.AsInt() != 20 { t.Errorf("complex: got %v", v) }
|
||||
|
||||
v = th.MacroEval(`"abc" + "def"`)
|
||||
if v.AsString() != "abcdef" { t.Errorf("strcat: got %q", v.AsString()) }
|
||||
}
|
||||
|
||||
func TestMacroEval_FunctionCall(t *testing.T) {
|
||||
vm := NewVM()
|
||||
|
||||
// Register a test function
|
||||
vm.RegisterSymbol(&Symbol{
|
||||
Name: "DOUBLE",
|
||||
Func: func(t *Thread) {
|
||||
t.Frame(1, 0)
|
||||
defer t.EndProc()
|
||||
n := t.Local(1).AsInt()
|
||||
t.PushInt(n * 2)
|
||||
t.RetValue()
|
||||
},
|
||||
})
|
||||
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval("Double(21)")
|
||||
if v.AsInt() != 42 { t.Errorf("funcall: got %v", v) }
|
||||
}
|
||||
|
||||
func TestMacroEval_Array(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval(`{1, 2, 3}`)
|
||||
if !v.IsArray() { t.Fatalf("array: not array") }
|
||||
arr := v.AsArray()
|
||||
if len(arr.Items) != 3 { t.Fatalf("array: len=%d", len(arr.Items)) }
|
||||
if arr.Items[0].AsInt() != 1 || arr.Items[2].AsInt() != 3 {
|
||||
t.Errorf("array: got %v %v", arr.Items[0], arr.Items[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMacroEval_Empty(t *testing.T) {
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
v := th.MacroEval("")
|
||||
if !v.IsNil() { t.Errorf("empty: got %v", v) }
|
||||
|
||||
v = th.MacroEval(" ")
|
||||
if !v.IsNil() { t.Errorf("whitespace: got %v", v) }
|
||||
}
|
||||
378
hbrt/ops_arith.go
Normal file
378
hbrt/ops_arith.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Arithmetic operations for the Five runtime.
|
||||
// Implements Harbour-compatible type promotion, overflow detection,
|
||||
// and decimal precision propagation rules.
|
||||
//
|
||||
// See docs/harbour-type-system-analysis.md Section 5 for details.
|
||||
package hbrt
|
||||
|
||||
import "math"
|
||||
|
||||
// Plus pops two values, pushes their sum.
|
||||
// Harbour: hb_vmPlus (hvm.c:3285)
|
||||
//
|
||||
// Type rules:
|
||||
// NumInt + NumInt -> NumInt (overflow -> Double)
|
||||
// Numeric + Numeric -> Double
|
||||
// String + String -> String (concatenation)
|
||||
// Date + Numeric -> Date
|
||||
// Timestamp + Numeric -> Timestamp
|
||||
func (t *Thread) Plus() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
// Fast path: Int + Int
|
||||
if a.IsNumInt() && b.IsNumInt() {
|
||||
an, bn := a.AsNumInt(), b.AsNumInt()
|
||||
r := an + bn
|
||||
// Overflow detection (Harbour pattern)
|
||||
if (bn >= 0 && r >= an) || (bn < 0 && r < an) {
|
||||
t.push(MakeNumInt(r))
|
||||
} else {
|
||||
t.push(MakeDoubleAuto(float64(an) + float64(bn)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Numeric + Numeric -> Double
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
ad, bd := a.AsNumDouble(), b.AsNumDouble()
|
||||
dec := maxDec(a.Decimal(), b.Decimal())
|
||||
t.push(MakeDouble(ad+bd, 255, dec))
|
||||
return
|
||||
}
|
||||
|
||||
// String + String -> concatenation
|
||||
if a.IsString() && b.IsString() {
|
||||
t.push(MakeString(a.AsString() + b.AsString()))
|
||||
return
|
||||
}
|
||||
|
||||
// Date + NumInt -> Date (add days)
|
||||
if a.IsDate() && b.IsNumInt() {
|
||||
t.push(MakeDate(a.AsJulian() + b.AsNumInt()))
|
||||
return
|
||||
}
|
||||
if a.IsNumInt() && b.IsDate() {
|
||||
t.push(MakeDate(a.AsNumInt() + b.AsJulian()))
|
||||
return
|
||||
}
|
||||
|
||||
// Timestamp + Numeric -> Timestamp
|
||||
if a.IsTimestamp() && b.IsNumeric() {
|
||||
days := int64(b.AsNumDouble())
|
||||
frac := b.AsNumDouble() - float64(days)
|
||||
ms := int32(frac * 86400000.0)
|
||||
newJulian := a.AsJulian() + days
|
||||
newTime := a.AsTimeMs() + ms
|
||||
if newTime >= 86400000 {
|
||||
newJulian++
|
||||
newTime -= 86400000
|
||||
} else if newTime < 0 {
|
||||
newJulian--
|
||||
newTime += 86400000
|
||||
}
|
||||
t.push(MakeTimestamp(newJulian, newTime))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("+", a, b))
|
||||
}
|
||||
|
||||
// Minus pops two values, pushes their difference.
|
||||
// Harbour: hb_vmMinus (hvm.c:3401)
|
||||
func (t *Thread) Minus() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
// Fast path: Int - Int
|
||||
if a.IsNumInt() && b.IsNumInt() {
|
||||
an, bn := a.AsNumInt(), b.AsNumInt()
|
||||
r := an - bn
|
||||
if (bn <= 0 && r >= an) || (bn > 0 && r < an) {
|
||||
t.push(MakeNumInt(r))
|
||||
} else {
|
||||
t.push(MakeDoubleAuto(float64(an) - float64(bn)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Numeric - Numeric -> Double
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
ad, bd := a.AsNumDouble(), b.AsNumDouble()
|
||||
dec := maxDec(a.Decimal(), b.Decimal())
|
||||
t.push(MakeDouble(ad-bd, 255, dec))
|
||||
return
|
||||
}
|
||||
|
||||
// Date - Date -> Long (difference in days)
|
||||
if a.IsDate() && b.IsDate() {
|
||||
t.push(MakeLong(a.AsJulian() - b.AsJulian()))
|
||||
return
|
||||
}
|
||||
|
||||
// Date - NumInt -> Date
|
||||
if a.IsDate() && b.IsNumInt() {
|
||||
t.push(MakeDate(a.AsJulian() - b.AsNumInt()))
|
||||
return
|
||||
}
|
||||
|
||||
// Timestamp - Timestamp -> Double or Long
|
||||
if a.IsTimestamp() && b.IsTimestamp() {
|
||||
dayDiff := a.AsJulian() - b.AsJulian()
|
||||
timeDiff := a.AsTimeMs() - b.AsTimeMs()
|
||||
if timeDiff != 0 {
|
||||
t.push(MakeDoubleAuto(float64(dayDiff) + float64(timeDiff)/86400000.0))
|
||||
} else {
|
||||
t.push(MakeLong(dayDiff))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("-", a, b))
|
||||
}
|
||||
|
||||
// Mult pops two values, pushes their product.
|
||||
// Harbour: hb_vmMult (hvm.c:3510)
|
||||
// Decimal rule: dec = dec1 + dec2
|
||||
func (t *Thread) Mult() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
if a.IsNumInt() && b.IsNumInt() {
|
||||
an, bn := a.AsNumInt(), b.AsNumInt()
|
||||
if an == 0 || bn == 0 {
|
||||
t.push(MakeNumInt(0))
|
||||
return
|
||||
}
|
||||
r := an * bn
|
||||
if r/an == bn {
|
||||
t.push(MakeNumInt(r))
|
||||
} else {
|
||||
t.push(MakeDoubleAuto(float64(an) * float64(bn)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
ad, bd := a.AsNumDouble(), b.AsNumDouble()
|
||||
dec := a.Decimal() + b.Decimal()
|
||||
if dec > 255 {
|
||||
dec = 255
|
||||
}
|
||||
t.push(MakeDouble(ad*bd, 255, dec))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("*", a, b))
|
||||
}
|
||||
|
||||
// Divide pops two values, pushes the quotient.
|
||||
// Harbour: hb_vmDivide (hvm.c:3546)
|
||||
// Always returns Double. Division by zero -> runtime error.
|
||||
func (t *Thread) Divide() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
bd := b.AsNumDouble()
|
||||
if bd == 0 {
|
||||
panic(t.divisionByZero())
|
||||
}
|
||||
ad := a.AsNumDouble()
|
||||
t.push(MakeDoubleAuto(ad / bd))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("/", a, b))
|
||||
}
|
||||
|
||||
// Modulus pops two values, pushes the remainder.
|
||||
// Harbour: hb_vmModulus (hvm.c:3608)
|
||||
// Always returns Double.
|
||||
func (t *Thread) Modulus() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
bd := b.AsNumDouble()
|
||||
if bd == 0 {
|
||||
panic(t.divisionByZero())
|
||||
}
|
||||
ad := a.AsNumDouble()
|
||||
t.push(MakeDoubleAuto(math.Mod(ad, bd)))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("%", a, b))
|
||||
}
|
||||
|
||||
// Power pops two values, pushes base^exponent.
|
||||
// Harbour: hb_vmPower
|
||||
// Always returns Double.
|
||||
func (t *Thread) Power() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
ad, bd := a.AsNumDouble(), b.AsNumDouble()
|
||||
t.push(MakeDoubleAuto(math.Pow(ad, bd)))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("**", a, b))
|
||||
}
|
||||
|
||||
// Negate negates the top of stack.
|
||||
// Harbour: hb_vmNegate
|
||||
func (t *Thread) Negate() {
|
||||
a := t.pop()
|
||||
|
||||
if a.IsNumInt() {
|
||||
t.push(MakeNumInt(-a.AsNumInt()))
|
||||
return
|
||||
}
|
||||
if a.IsDouble() {
|
||||
t.push(MakeDouble(-a.AsDouble(), a.Length(), a.Decimal()))
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("negate", a))
|
||||
}
|
||||
|
||||
// Inc increments the top of stack by 1.
|
||||
// Harbour: hb_vmInc
|
||||
func (t *Thread) Inc() {
|
||||
a := t.pop()
|
||||
if a.IsNumInt() {
|
||||
t.push(MakeNumInt(a.AsNumInt() + 1))
|
||||
return
|
||||
}
|
||||
if a.IsDouble() {
|
||||
t.push(MakeDouble(a.AsDouble()+1, a.Length(), a.Decimal()))
|
||||
return
|
||||
}
|
||||
panic(t.argError("++", a))
|
||||
}
|
||||
|
||||
// Dec decrements the top of stack by 1.
|
||||
// Harbour: hb_vmDec
|
||||
func (t *Thread) Dec() {
|
||||
a := t.pop()
|
||||
if a.IsNumInt() {
|
||||
t.push(MakeNumInt(a.AsNumInt() - 1))
|
||||
return
|
||||
}
|
||||
if a.IsDouble() {
|
||||
t.push(MakeDouble(a.AsDouble()-1, a.Length(), a.Decimal()))
|
||||
return
|
||||
}
|
||||
panic(t.argError("--", a))
|
||||
}
|
||||
|
||||
// --- Optimized operations (used by generated code) ---
|
||||
|
||||
// AddInt adds an integer constant to the top of stack.
|
||||
// Harbour: hb_xvmAddInt
|
||||
func (t *Thread) AddInt(n int64) {
|
||||
a := t.pop()
|
||||
if a.IsNumInt() {
|
||||
an := a.AsNumInt()
|
||||
r := an + n
|
||||
if (n >= 0 && r >= an) || (n < 0 && r < an) {
|
||||
t.push(MakeNumInt(r))
|
||||
} else {
|
||||
t.push(MakeDoubleAuto(float64(an) + float64(n)))
|
||||
}
|
||||
return
|
||||
}
|
||||
if a.IsDouble() {
|
||||
t.push(MakeDouble(a.AsDouble()+float64(n), a.Length(), a.Decimal()))
|
||||
return
|
||||
}
|
||||
if a.IsDate() {
|
||||
t.push(MakeDate(a.AsJulian() + n))
|
||||
return
|
||||
}
|
||||
panic(t.argError("+int", a))
|
||||
}
|
||||
|
||||
// LocalAdd adds the top of stack to a local variable, pops the value.
|
||||
// Harbour: hb_xvmLocalAdd
|
||||
func (t *Thread) LocalAdd(n int) {
|
||||
val := t.pop()
|
||||
idx := t.localIndex(n)
|
||||
loc := t.locals[idx]
|
||||
|
||||
if loc.IsNumInt() && val.IsNumInt() {
|
||||
r := loc.AsNumInt() + val.AsNumInt()
|
||||
if (val.AsNumInt() >= 0 && r >= loc.AsNumInt()) || (val.AsNumInt() < 0 && r < loc.AsNumInt()) {
|
||||
t.locals[idx] = MakeNumInt(r)
|
||||
} else {
|
||||
t.locals[idx] = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val.AsNumInt()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if loc.IsNumeric() && val.IsNumeric() {
|
||||
dec := maxDec(loc.Decimal(), val.Decimal())
|
||||
t.locals[idx] = MakeDouble(loc.AsNumDouble()+val.AsNumDouble(), 255, dec)
|
||||
return
|
||||
}
|
||||
|
||||
if loc.IsString() && val.IsString() {
|
||||
t.locals[idx] = MakeString(loc.AsString() + val.AsString())
|
||||
return
|
||||
}
|
||||
|
||||
panic(t.argError("+=", loc, val))
|
||||
}
|
||||
|
||||
// LocalAddInt adds an integer constant directly to a local variable.
|
||||
// Harbour: hb_xvmLocalAddInt (fused PUSHINT + PLUS + POPLOCAL)
|
||||
func (t *Thread) LocalAddInt(n int, val int64) {
|
||||
idx := t.localIndex(n)
|
||||
loc := t.locals[idx]
|
||||
|
||||
if loc.IsNumInt() {
|
||||
r := loc.AsNumInt() + val
|
||||
if (val >= 0 && r >= loc.AsNumInt()) || (val < 0 && r < loc.AsNumInt()) {
|
||||
t.locals[idx] = MakeNumInt(r)
|
||||
} else {
|
||||
t.locals[idx] = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val))
|
||||
}
|
||||
return
|
||||
}
|
||||
if loc.IsDouble() {
|
||||
t.locals[idx] = MakeDouble(loc.AsDouble()+float64(val), loc.Length(), loc.Decimal())
|
||||
return
|
||||
}
|
||||
if loc.IsDate() {
|
||||
t.locals[idx] = MakeDate(loc.AsJulian() + val)
|
||||
return
|
||||
}
|
||||
panic(t.argError("+int", loc))
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func maxDec(a, b uint16) uint16 {
|
||||
if a == 255 || b == 255 {
|
||||
return 255 // HB_DEFAULT_DECIMALS
|
||||
}
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (t *Thread) divisionByZero() *HbError {
|
||||
return &HbError{
|
||||
Description: "division by zero",
|
||||
Operation: "/",
|
||||
SubSystem: "BASE",
|
||||
GenCode: 1340, // EG_ZERODIV
|
||||
}
|
||||
}
|
||||
359
hbrt/ops_arith_test.go
Normal file
359
hbrt/ops_arith_test.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- Plus ---
|
||||
|
||||
func TestPlusIntInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(10)
|
||||
th.PushInt(20)
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsNumInt() || r.AsNumInt() != 30 {
|
||||
t.Errorf("10 + 20 = %v, want 30", r)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPlusIntOverflow(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushLong(math.MaxInt64)
|
||||
th.PushLong(1)
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDouble() {
|
||||
t.Errorf("MaxInt64 + 1 should overflow to Double, got type %d", r.Type())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPlusDoubleDouble(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushDouble(1.5, 3, 1)
|
||||
th.PushDouble(2.3, 3, 1)
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDouble() {
|
||||
t.Fatal("expected Double")
|
||||
}
|
||||
if math.Abs(r.AsDouble()-3.8) > 1e-10 {
|
||||
t.Errorf("1.5 + 2.3 = %g, want 3.8", r.AsDouble())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPlusStringConcat(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushString("Hello, ")
|
||||
th.PushString("World!")
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
if r.AsString() != "Hello, World!" {
|
||||
t.Errorf("string concat = %q, want %q", r.AsString(), "Hello, World!")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPlusDateInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.push(MakeDate(2461033)) // 2026-03-27
|
||||
th.PushInt(10)
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDate() || r.AsJulian() != 2461043 {
|
||||
t.Errorf("Date + 10 = %v, want Date(2461043)", r)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPlusDecimalPropagation(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushDouble(1.2, 4, 1) // 1 decimal
|
||||
th.PushDouble(3.456, 5, 3) // 3 decimals
|
||||
th.Plus()
|
||||
|
||||
r := th.pop()
|
||||
// Result should have max(1, 3) = 3 decimals
|
||||
if r.Decimal() != 3 {
|
||||
t.Errorf("decimal = %d, want 3", r.Decimal())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Minus ---
|
||||
|
||||
func TestMinusIntInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(30)
|
||||
th.PushInt(20)
|
||||
th.Minus()
|
||||
|
||||
r := th.pop()
|
||||
if r.AsNumInt() != 10 {
|
||||
t.Errorf("30 - 20 = %d, want 10", r.AsNumInt())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestMinusDateDate(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.push(MakeDate(2461043))
|
||||
th.push(MakeDate(2461033))
|
||||
th.Minus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsLong() || r.AsLong() != 10 {
|
||||
t.Errorf("Date - Date = %v, want 10", r)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Mult ---
|
||||
|
||||
func TestMultIntInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(6)
|
||||
th.PushInt(7)
|
||||
th.Mult()
|
||||
|
||||
r := th.pop()
|
||||
if r.AsNumInt() != 42 {
|
||||
t.Errorf("6 * 7 = %d, want 42", r.AsNumInt())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestMultDecimalRule(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushDouble(1.5, 3, 1) // 1 decimal
|
||||
th.PushDouble(2.5, 3, 1) // 1 decimal
|
||||
th.Mult()
|
||||
|
||||
r := th.pop()
|
||||
// Mult decimal = dec1 + dec2 = 2
|
||||
if r.Decimal() != 2 {
|
||||
t.Errorf("decimal = %d, want 2", r.Decimal())
|
||||
}
|
||||
if math.Abs(r.AsDouble()-3.75) > 1e-10 {
|
||||
t.Errorf("1.5 * 2.5 = %g, want 3.75", r.AsDouble())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestMultIntOverflow(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushLong(math.MaxInt64)
|
||||
th.PushLong(2)
|
||||
th.Mult()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDouble() {
|
||||
t.Errorf("MaxInt64 * 2 should overflow to Double")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Divide ---
|
||||
|
||||
func TestDivide(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(10)
|
||||
th.PushInt(3)
|
||||
th.Divide()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDouble() {
|
||||
t.Error("division should always return Double")
|
||||
}
|
||||
if math.Abs(r.AsDouble()-3.333333) > 0.001 {
|
||||
t.Errorf("10 / 3 = %g", r.AsDouble())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestDivideByZero(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Error("expected panic on division by zero")
|
||||
}
|
||||
hbErr, ok := r.(*HbError)
|
||||
if !ok {
|
||||
t.Errorf("expected HbError, got %T", r)
|
||||
}
|
||||
if hbErr.GenCode != 1340 {
|
||||
t.Errorf("GenCode = %d, want 1340 (EG_ZERODIV)", hbErr.GenCode)
|
||||
}
|
||||
}()
|
||||
|
||||
th.PushInt(10)
|
||||
th.PushInt(0)
|
||||
th.Divide()
|
||||
}
|
||||
|
||||
// --- Modulus ---
|
||||
|
||||
func TestModulus(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(10)
|
||||
th.PushInt(3)
|
||||
th.Modulus()
|
||||
|
||||
r := th.pop()
|
||||
if !r.IsDouble() || r.AsDouble() != 1.0 {
|
||||
t.Errorf("10 %% 3 = %v, want 1.0", r)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Power ---
|
||||
|
||||
func TestPower(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(2)
|
||||
th.PushInt(10)
|
||||
th.Power()
|
||||
|
||||
r := th.pop()
|
||||
if r.AsDouble() != 1024.0 {
|
||||
t.Errorf("2 ** 10 = %g, want 1024", r.AsDouble())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Negate ---
|
||||
|
||||
func TestNegate(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(42)
|
||||
th.Negate()
|
||||
if th.pop().AsNumInt() != -42 {
|
||||
t.Error("negate 42 should be -42")
|
||||
}
|
||||
|
||||
th.PushDouble(3.14, 4, 2)
|
||||
th.Negate()
|
||||
r := th.pop()
|
||||
if r.AsDouble() != -3.14 {
|
||||
t.Errorf("negate 3.14 = %g", r.AsDouble())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Inc / Dec ---
|
||||
|
||||
func TestIncDec(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(10)
|
||||
th.Inc()
|
||||
if th.peek().AsNumInt() != 11 {
|
||||
t.Error("Inc(10) should be 11")
|
||||
}
|
||||
th.Dec()
|
||||
th.Dec()
|
||||
if th.pop().AsNumInt() != 9 {
|
||||
t.Error("Dec(Dec(11)) should be 9")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- AddInt optimization ---
|
||||
|
||||
func TestAddInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(100)
|
||||
th.AddInt(50)
|
||||
if th.pop().AsNumInt() != 150 {
|
||||
t.Error("100 + 50 should be 150")
|
||||
}
|
||||
|
||||
th.push(MakeDate(2461033))
|
||||
th.AddInt(7)
|
||||
r := th.pop()
|
||||
if !r.IsDate() || r.AsJulian() != 2461040 {
|
||||
t.Errorf("Date + 7 = %v", r)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- LocalAdd optimization ---
|
||||
|
||||
func TestLocalAdd(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 2)
|
||||
|
||||
th.LocalSetInt(1, 100)
|
||||
th.PushInt(50)
|
||||
th.LocalAdd(1)
|
||||
|
||||
if th.Local(1).AsNumInt() != 150 {
|
||||
t.Errorf("local += 50: got %d, want 150", th.Local(1).AsNumInt())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- LocalAddInt optimization ---
|
||||
|
||||
func TestLocalAddInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 2)
|
||||
|
||||
th.LocalSetInt(1, 0)
|
||||
for i := int64(1); i <= 10; i++ {
|
||||
th.LocalAddInt(1, i)
|
||||
}
|
||||
|
||||
if th.Local(1).AsNumInt() != 55 {
|
||||
t.Errorf("sum 1..10 = %d, want 55", th.Local(1).AsNumInt())
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
108
hbrt/ops_collection.go
Normal file
108
hbrt/ops_collection.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Collection operations for the Five runtime.
|
||||
// Array generation, indexing, hash generation, and code block evaluation.
|
||||
// Harbour: HB_P_ARRAYGEN, HB_P_ARRAYPUSH, HB_P_ARRAYPOP, etc.
|
||||
package hbrt
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// ArrayGen pops n items from stack and creates an array.
|
||||
// Harbour: HB_P_ARRAYGEN / hb_vmArrayGen
|
||||
func (t *Thread) ArrayGen(n int) {
|
||||
items := make([]Value, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
items[i] = t.pop()
|
||||
}
|
||||
t.push(MakeArrayFrom(items))
|
||||
}
|
||||
|
||||
// HashGen pops n key-value pairs and creates a hash.
|
||||
// Stack: [key1] [val1] [key2] [val2] ... → Hash
|
||||
func (t *Thread) HashGen(n int) {
|
||||
hh := &HbHash{
|
||||
Keys: make([]Value, n),
|
||||
Values: make([]Value, n),
|
||||
}
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
hh.Values[i] = t.pop()
|
||||
hh.Keys[i] = t.pop()
|
||||
}
|
||||
t.push(Value{
|
||||
info: makeInfo(tHash, 0, 0),
|
||||
ptr: unsafe.Pointer(hh),
|
||||
})
|
||||
}
|
||||
|
||||
// ArrayPush pops index and array, pushes array[index].
|
||||
// Harbour: HB_P_ARRAYPUSH
|
||||
func (t *Thread) ArrayPush() {
|
||||
idx := t.pop()
|
||||
arr := t.pop()
|
||||
|
||||
if !arr.IsArray() {
|
||||
panic(t.argError("[]", arr, idx))
|
||||
}
|
||||
ha := arr.AsArray()
|
||||
n := int(idx.AsNumInt())
|
||||
|
||||
// Harbour: 1-based indexing
|
||||
if n < 1 || n > len(ha.Items) {
|
||||
panic(t.runtimeError("array index out of bounds"))
|
||||
}
|
||||
t.push(ha.Items[n-1])
|
||||
}
|
||||
|
||||
// ArrayPop pops value, index, array and sets array[index] = value.
|
||||
// Harbour: HB_P_ARRAYPOP
|
||||
func (t *Thread) ArrayPop() {
|
||||
val := t.pop()
|
||||
idx := t.pop()
|
||||
arr := t.pop()
|
||||
|
||||
if !arr.IsArray() {
|
||||
panic(t.argError("[]=", arr, idx))
|
||||
}
|
||||
ha := arr.AsArray()
|
||||
n := int(idx.AsNumInt())
|
||||
|
||||
if n < 1 || n > len(ha.Items) {
|
||||
panic(t.runtimeError("array index out of bounds"))
|
||||
}
|
||||
ha.Items[n-1] = val
|
||||
}
|
||||
|
||||
// EvalBlock evaluates a code block on the stack with nArgs arguments.
|
||||
// Stack: [block] [arg1] ... [argN] → [result]
|
||||
func (t *Thread) EvalBlock(nArgs int) {
|
||||
args := make([]Value, nArgs)
|
||||
for i := nArgs - 1; i >= 0; i-- {
|
||||
args[i] = t.pop()
|
||||
}
|
||||
blockVal := t.pop()
|
||||
if !blockVal.IsBlock() {
|
||||
panic(t.argError("Eval", blockVal))
|
||||
}
|
||||
blk := blockVal.AsBlock()
|
||||
|
||||
// Push args for Frame
|
||||
for _, arg := range args {
|
||||
t.push(arg)
|
||||
}
|
||||
|
||||
t.pendingParams = nArgs
|
||||
blk.Fn(t)
|
||||
t.push(t.retVal)
|
||||
}
|
||||
|
||||
// PushBlock creates a code block and pushes it onto the stack.
|
||||
func (t *Thread) PushBlock(fn func(*Thread), detachedLocals int) {
|
||||
t.push(MakeBlock(fn, detachedLocals))
|
||||
}
|
||||
|
||||
// PushSelf pushes the current Self object (for :: access in methods).
|
||||
func (t *Thread) PushSelf() {
|
||||
t.push(t.self)
|
||||
}
|
||||
|
||||
302
hbrt/ops_compare.go
Normal file
302
hbrt/ops_compare.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Comparison and logical operations for the Five runtime.
|
||||
// Implements Harbour-compatible comparison semantics including:
|
||||
// - NIL == NIL → true, NIL == anything → false
|
||||
// - Numeric cross-type comparison (Int/Long/Double auto-promotion)
|
||||
// - String comparison respecting SET EXACT
|
||||
// - Date/Timestamp comparison
|
||||
// - Logical XOR equality (Clipper quirk)
|
||||
//
|
||||
// Operator hierarchy inspired by tsgo's isEqualityOperator/isRelationalOperatorOrHigher
|
||||
// pattern (ref/typescript-go/internal/checker/utilities.go:772).
|
||||
//
|
||||
// See docs/harbour-type-system-analysis.md Section 6 for full rules.
|
||||
package hbrt
|
||||
|
||||
import "strings"
|
||||
|
||||
// --- Equality operators ---
|
||||
|
||||
// Equal pops two values, pushes boolean result.
|
||||
// Harbour: hb_vmEqual (hvm.c:3974)
|
||||
func (t *Thread) Equal() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
t.push(MakeBool(valueEqual(a, b)))
|
||||
}
|
||||
|
||||
// ExactEqual pops two values, pushes boolean result.
|
||||
// Harbour: hb_vmExactlyEqual — arrays cannot override.
|
||||
func (t *Thread) ExactEqual() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
t.push(MakeBool(valueExactEqual(a, b)))
|
||||
}
|
||||
|
||||
// NotEqual pops two values, pushes boolean result.
|
||||
func (t *Thread) NotEqual() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
t.push(MakeBool(!valueEqual(a, b)))
|
||||
}
|
||||
|
||||
// --- Relational operators ---
|
||||
|
||||
// Less pops two values, pushes boolean result.
|
||||
// Harbour: hb_vmLess (hvm.c:4176)
|
||||
func (t *Thread) Less() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
cmp, ok := valueCompare(a, b)
|
||||
if !ok {
|
||||
panic(t.argError("<", a, b))
|
||||
}
|
||||
t.push(MakeBool(cmp < 0))
|
||||
}
|
||||
|
||||
// LessEqual pops two values, pushes boolean result.
|
||||
func (t *Thread) LessEqual() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
cmp, ok := valueCompare(a, b)
|
||||
if !ok {
|
||||
panic(t.argError("<=", a, b))
|
||||
}
|
||||
t.push(MakeBool(cmp <= 0))
|
||||
}
|
||||
|
||||
// Greater pops two values, pushes boolean result.
|
||||
func (t *Thread) Greater() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
cmp, ok := valueCompare(a, b)
|
||||
if !ok {
|
||||
panic(t.argError(">", a, b))
|
||||
}
|
||||
t.push(MakeBool(cmp > 0))
|
||||
}
|
||||
|
||||
// GreaterEqual pops two values, pushes boolean result.
|
||||
func (t *Thread) GreaterEqual() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
cmp, ok := valueCompare(a, b)
|
||||
if !ok {
|
||||
panic(t.argError(">=", a, b))
|
||||
}
|
||||
t.push(MakeBool(cmp >= 0))
|
||||
}
|
||||
|
||||
// --- Logical operators ---
|
||||
|
||||
// Not negates the boolean value on top of stack.
|
||||
func (t *Thread) Not() {
|
||||
a := t.pop()
|
||||
if !a.IsLogical() {
|
||||
panic(t.argError(".NOT.", a))
|
||||
}
|
||||
t.push(MakeBool(!a.AsBool()))
|
||||
}
|
||||
|
||||
// And pops two values, pushes logical AND.
|
||||
// Harbour evaluates both sides (no short-circuit in VM ops).
|
||||
func (t *Thread) And() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
if !a.IsLogical() || !b.IsLogical() {
|
||||
panic(t.argError(".AND.", a, b))
|
||||
}
|
||||
t.push(MakeBool(a.AsBool() && b.AsBool()))
|
||||
}
|
||||
|
||||
// Or pops two values, pushes logical OR.
|
||||
func (t *Thread) Or() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
if !a.IsLogical() || !b.IsLogical() {
|
||||
panic(t.argError(".OR.", a, b))
|
||||
}
|
||||
t.push(MakeBool(a.AsBool() || b.AsBool()))
|
||||
}
|
||||
|
||||
// InString implements the $ operator: "bc" $ "abcde" → .T.
|
||||
func (t *Thread) InString() {
|
||||
b := t.pop()
|
||||
a := t.pop()
|
||||
if a.IsString() && b.IsString() {
|
||||
t.push(MakeBool(strings.Contains(b.AsString(), a.AsString())))
|
||||
} else {
|
||||
panic(t.argError("$", a, b))
|
||||
}
|
||||
}
|
||||
|
||||
// PopLogical pops the top of stack and returns it as bool.
|
||||
// Used by generated code for IF/WHILE conditions.
|
||||
// Harbour: hb_xvmPopLogical
|
||||
func (t *Thread) PopLogical() bool {
|
||||
v := t.pop()
|
||||
if !v.IsLogical() {
|
||||
panic(t.argError("logical", v))
|
||||
}
|
||||
return v.AsBool()
|
||||
}
|
||||
|
||||
// --- Optimized comparison (used by generated code) ---
|
||||
|
||||
// EqualIntIs compares stack top with an integer constant, returns bool.
|
||||
// Harbour: hb_xvmEqualIntIs (fused PUSHINT + EQUAL)
|
||||
func (t *Thread) EqualIntIs(n int64) bool {
|
||||
a := t.pop()
|
||||
if a.IsNumInt() {
|
||||
return a.AsNumInt() == n
|
||||
}
|
||||
if a.IsDouble() {
|
||||
return a.AsDouble() == float64(n)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- Internal comparison functions ---
|
||||
|
||||
// valueEqual implements Harbour's equality semantics.
|
||||
// NIL == NIL → true; NIL == anything → false
|
||||
// Numeric: cross-type double comparison
|
||||
// String: case-sensitive (SET EXACT ON assumed for now)
|
||||
// Logical: XOR-like (Clipper quirk)
|
||||
// Array/Hash/Block/Pointer: pointer identity
|
||||
func valueEqual(a, b Value) bool {
|
||||
at, bt := a.Type(), b.Type()
|
||||
|
||||
// NIL handling
|
||||
if at == tNil && bt == tNil {
|
||||
return true
|
||||
}
|
||||
if at == tNil || bt == tNil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Numeric cross-type comparison
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
if a.IsNumInt() && b.IsNumInt() {
|
||||
return a.AsNumInt() == b.AsNumInt()
|
||||
}
|
||||
return a.AsNumDouble() == b.AsNumDouble()
|
||||
}
|
||||
|
||||
// Same type required from here
|
||||
if at != bt {
|
||||
return false
|
||||
}
|
||||
|
||||
switch at {
|
||||
case tString:
|
||||
return a.AsString() == b.AsString()
|
||||
|
||||
case tDate:
|
||||
return a.AsJulian() == b.AsJulian()
|
||||
|
||||
case tTimestamp:
|
||||
return a.AsJulian() == b.AsJulian() && a.AsTimeMs() == b.AsTimeMs()
|
||||
|
||||
case tLogical:
|
||||
// Harbour/Clipper quirk: XOR-like behavior
|
||||
// .T. = .T. → .T. .F. = .F. → .T.
|
||||
// .T. = .F. → .F. .F. = .T. → .F.
|
||||
return a.AsBool() == b.AsBool()
|
||||
|
||||
case tArray, tObject:
|
||||
// Pointer identity
|
||||
return a.ptr == b.ptr
|
||||
|
||||
case tHash:
|
||||
return a.ptr == b.ptr
|
||||
|
||||
case tBlock:
|
||||
return a.ptr == b.ptr
|
||||
|
||||
case tPointer:
|
||||
return a.scalar == b.scalar
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// valueExactEqual is like valueEqual but arrays/objects cannot override.
|
||||
// Harbour: hb_vmExactlyEqual
|
||||
func valueExactEqual(a, b Value) bool {
|
||||
// For strings, exact equality checks full length (ignoring SET EXACT)
|
||||
if a.IsString() && b.IsString() {
|
||||
return a.AsString() == b.AsString()
|
||||
}
|
||||
return valueEqual(a, b)
|
||||
}
|
||||
|
||||
// valueCompare returns comparison result (-1, 0, +1) and whether comparison is valid.
|
||||
// Only String, Numeric, Date, Timestamp support ordering.
|
||||
// Harbour: hb_vmLess, hb_vmGreater, etc. (hvm.c:4176+)
|
||||
func valueCompare(a, b Value) (int, bool) {
|
||||
// Numeric comparison
|
||||
if a.IsNumeric() && b.IsNumeric() {
|
||||
if a.IsNumInt() && b.IsNumInt() {
|
||||
return compareInt64(a.AsNumInt(), b.AsNumInt()), true
|
||||
}
|
||||
return compareFloat64(a.AsNumDouble(), b.AsNumDouble()), true
|
||||
}
|
||||
|
||||
at, bt := a.Type(), b.Type()
|
||||
if at != bt {
|
||||
return 0, false // type mismatch → error
|
||||
}
|
||||
|
||||
switch at {
|
||||
case tString:
|
||||
return strings.Compare(a.AsString(), b.AsString()), true
|
||||
|
||||
case tDate:
|
||||
return compareInt64(a.AsJulian(), b.AsJulian()), true
|
||||
|
||||
case tTimestamp:
|
||||
cmp := compareInt64(a.AsJulian(), b.AsJulian())
|
||||
if cmp != 0 {
|
||||
return cmp, true
|
||||
}
|
||||
return compareInt32(a.AsTimeMs(), b.AsTimeMs()), true
|
||||
}
|
||||
|
||||
return 0, false // unsupported type for ordering
|
||||
}
|
||||
|
||||
// --- Primitive comparison helpers ---
|
||||
// Following tsgo pattern of small, inlineable helper functions.
|
||||
|
||||
func compareInt64(a, b int64) int {
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
if a > b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func compareInt32(a, b int32) int {
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
if a > b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func compareFloat64(a, b float64) int {
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
if a > b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
453
hbrt/ops_compare_test.go
Normal file
453
hbrt/ops_compare_test.go
Normal file
@@ -0,0 +1,453 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import "testing"
|
||||
|
||||
// --- Equal ---
|
||||
|
||||
func TestEqualNilNil(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushNil()
|
||||
th.PushNil()
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("NIL == NIL should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualNilOther(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushNil()
|
||||
th.PushInt(0)
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("NIL == 0 should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualIntInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(42)
|
||||
th.PushInt(42)
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("42 == 42 should be true")
|
||||
}
|
||||
|
||||
th.PushInt(42)
|
||||
th.PushInt(99)
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("42 == 99 should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualIntDouble(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
// Cross-type numeric: Int == Double
|
||||
th.PushInt(42)
|
||||
th.PushDouble(42.0, 4, 1)
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("42 == 42.0 should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualString(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushString("hello")
|
||||
th.PushString("hello")
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error(`"hello" == "hello" should be true`)
|
||||
}
|
||||
|
||||
th.PushString("hello")
|
||||
th.PushString("world")
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error(`"hello" == "world" should be false`)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualLogical(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushBool(true)
|
||||
th.PushBool(true)
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error(".T. == .T. should be true")
|
||||
}
|
||||
|
||||
th.PushBool(true)
|
||||
th.PushBool(false)
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error(".T. == .F. should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualDate(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.push(MakeDate(2461033))
|
||||
th.push(MakeDate(2461033))
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("same date should be equal")
|
||||
}
|
||||
|
||||
th.push(MakeDate(2461033))
|
||||
th.push(MakeDate(2461034))
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("different dates should not be equal")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualTimestamp(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.push(MakeTimestamp(2461033, 43200000))
|
||||
th.push(MakeTimestamp(2461033, 43200000))
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("same timestamp should be equal")
|
||||
}
|
||||
|
||||
// Same date, different time
|
||||
th.push(MakeTimestamp(2461033, 43200000))
|
||||
th.push(MakeTimestamp(2461033, 43200001))
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("different time should not be equal")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestEqualArrayIdentity(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
a := MakeArray(3)
|
||||
th.push(a)
|
||||
th.push(a) // same pointer
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("same array should be equal (pointer identity)")
|
||||
}
|
||||
|
||||
th.push(a)
|
||||
th.push(MakeArray(3)) // different pointer
|
||||
th.Equal()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("different arrays should not be equal")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- NotEqual ---
|
||||
|
||||
func TestNotEqual(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(1)
|
||||
th.PushInt(2)
|
||||
th.NotEqual()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("1 != 2 should be true")
|
||||
}
|
||||
|
||||
th.PushInt(5)
|
||||
th.PushInt(5)
|
||||
th.NotEqual()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("5 != 5 should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Relational ---
|
||||
|
||||
func TestLessInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(1)
|
||||
th.PushInt(2)
|
||||
th.Less()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("1 < 2 should be true")
|
||||
}
|
||||
|
||||
th.PushInt(2)
|
||||
th.PushInt(1)
|
||||
th.Less()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("2 < 1 should be false")
|
||||
}
|
||||
|
||||
th.PushInt(1)
|
||||
th.PushInt(1)
|
||||
th.Less()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("1 < 1 should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLessEqualInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(1)
|
||||
th.PushInt(1)
|
||||
th.LessEqual()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("1 <= 1 should be true")
|
||||
}
|
||||
|
||||
th.PushInt(2)
|
||||
th.PushInt(1)
|
||||
th.LessEqual()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("2 <= 1 should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestGreater(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(10)
|
||||
th.PushInt(5)
|
||||
th.Greater()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("10 > 5 should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestGreaterEqual(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(5)
|
||||
th.PushInt(5)
|
||||
th.GreaterEqual()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("5 >= 5 should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLessString(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushString("abc")
|
||||
th.PushString("def")
|
||||
th.Less()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error(`"abc" < "def" should be true`)
|
||||
}
|
||||
|
||||
th.PushString("def")
|
||||
th.PushString("abc")
|
||||
th.Less()
|
||||
if th.pop().AsBool() {
|
||||
t.Error(`"def" < "abc" should be false`)
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLessDate(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.push(MakeDate(2461033))
|
||||
th.push(MakeDate(2461034))
|
||||
th.Less()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("earlier date < later date should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLessTimestamp(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
// Same day, earlier time
|
||||
th.push(MakeTimestamp(2461033, 10000))
|
||||
th.push(MakeTimestamp(2461033, 20000))
|
||||
th.Less()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("earlier timestamp should be less")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLessTypeMismatch(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Error("expected panic on type mismatch comparison")
|
||||
}
|
||||
}()
|
||||
th.PushInt(1)
|
||||
th.PushString("hello")
|
||||
th.Less() // should panic
|
||||
}
|
||||
|
||||
// --- Logical operators ---
|
||||
|
||||
func TestNot(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushBool(true)
|
||||
th.Not()
|
||||
if th.pop().AsBool() {
|
||||
t.Error("NOT .T. should be .F.")
|
||||
}
|
||||
|
||||
th.PushBool(false)
|
||||
th.Not()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("NOT .F. should be .T.")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestAnd(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
tests := []struct {
|
||||
a, b, want bool
|
||||
}{
|
||||
{true, true, true},
|
||||
{true, false, false},
|
||||
{false, true, false},
|
||||
{false, false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
th.PushBool(tt.a)
|
||||
th.PushBool(tt.b)
|
||||
th.And()
|
||||
if th.pop().AsBool() != tt.want {
|
||||
t.Errorf("%v .AND. %v should be %v", tt.a, tt.b, tt.want)
|
||||
}
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestOr(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
tests := []struct {
|
||||
a, b, want bool
|
||||
}{
|
||||
{true, true, true},
|
||||
{true, false, true},
|
||||
{false, true, true},
|
||||
{false, false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
th.PushBool(tt.a)
|
||||
th.PushBool(tt.b)
|
||||
th.Or()
|
||||
if th.pop().AsBool() != tt.want {
|
||||
t.Errorf("%v .OR. %v should be %v", tt.a, tt.b, tt.want)
|
||||
}
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- PopLogical ---
|
||||
|
||||
func TestPopLogical(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushBool(true)
|
||||
if !th.PopLogical() {
|
||||
t.Error("PopLogical(.T.) should be true")
|
||||
}
|
||||
th.PushBool(false)
|
||||
if th.PopLogical() {
|
||||
t.Error("PopLogical(.F.) should be false")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPopLogicalPanicOnNonBool(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic on PopLogical with non-boolean")
|
||||
}
|
||||
}()
|
||||
th.PushInt(42)
|
||||
th.PopLogical()
|
||||
}
|
||||
|
||||
// --- EqualIntIs optimization ---
|
||||
|
||||
func TestEqualIntIs(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(10)
|
||||
if !th.EqualIntIs(10) {
|
||||
t.Error("10 == 10 should be true")
|
||||
}
|
||||
|
||||
th.PushInt(10)
|
||||
if th.EqualIntIs(20) {
|
||||
t.Error("10 == 20 should be false")
|
||||
}
|
||||
|
||||
th.PushDouble(10.0, 4, 1)
|
||||
if !th.EqualIntIs(10) {
|
||||
t.Error("10.0 == 10 should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Cross-type numeric comparison ---
|
||||
|
||||
func TestCompareIntVsLong(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(42)
|
||||
th.PushLong(42)
|
||||
th.Equal()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("Int(42) == Long(42) should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestCompareIntVsDouble(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
th.PushInt(5)
|
||||
th.PushDouble(10.0, 4, 1)
|
||||
th.Less()
|
||||
if !th.pop().AsBool() {
|
||||
t.Error("Int(5) < Double(10.0) should be true")
|
||||
}
|
||||
th.EndProc()
|
||||
}
|
||||
233
hbrt/pcinterp.go
Normal file
233
hbrt/pcinterp.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Five pcode interpreter — executes pcode bytecode on a Thread.
|
||||
// Each opcode directly calls the corresponding Thread method,
|
||||
// so pcode execution is semantically identical to gengo-compiled code.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// ExecPcode runs a pcode function on the given thread.
|
||||
func ExecPcode(t *Thread, fn *PcodeFunc, mod *PcodeModule) {
|
||||
code := fn.Code
|
||||
pc := 0 // program counter
|
||||
|
||||
t.Frame(fn.Params, fn.Locals)
|
||||
defer t.EndProc()
|
||||
|
||||
for pc < len(code) {
|
||||
op := code[pc]
|
||||
pc++
|
||||
|
||||
switch op {
|
||||
case PcOpNop:
|
||||
// do nothing
|
||||
|
||||
// --- Stack ---
|
||||
case PcOpPushNil:
|
||||
t.PushNil()
|
||||
case PcOpPushTrue:
|
||||
t.PushBool(true)
|
||||
case PcOpPushFalse:
|
||||
t.PushBool(false)
|
||||
case PcOpPushInt:
|
||||
v := int64(binary.LittleEndian.Uint64(code[pc:]))
|
||||
pc += 8
|
||||
t.PushLong(v)
|
||||
case PcOpPushDouble:
|
||||
bits := binary.LittleEndian.Uint64(code[pc:])
|
||||
pc += 8
|
||||
t.PushDouble(math.Float64frombits(bits), 0, 0)
|
||||
case PcOpPushString:
|
||||
slen := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.PushString(string(code[pc : pc+slen]))
|
||||
pc += slen
|
||||
case PcOpPushBool:
|
||||
t.PushBool(code[pc] != 0)
|
||||
pc++
|
||||
case PcOpPushLocal:
|
||||
idx := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.PushLocal(idx)
|
||||
case PcOpPopLocal:
|
||||
idx := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.PopLocal(idx)
|
||||
case PcOpPop:
|
||||
t.Pop()
|
||||
case PcOpDup:
|
||||
t.Dup()
|
||||
|
||||
// --- Arithmetic ---
|
||||
case PcOpPlus:
|
||||
t.Plus()
|
||||
case PcOpMinus:
|
||||
t.Minus()
|
||||
case PcOpMult:
|
||||
t.Mult()
|
||||
case PcOpDivide:
|
||||
t.Divide()
|
||||
case PcOpMod:
|
||||
t.Modulus()
|
||||
case PcOpPower:
|
||||
t.Power()
|
||||
case PcOpNegate:
|
||||
t.Negate()
|
||||
|
||||
// --- Comparison ---
|
||||
case PcOpEqual:
|
||||
t.Equal()
|
||||
case PcOpNotEqual:
|
||||
t.NotEqual()
|
||||
case PcOpLess:
|
||||
t.Less()
|
||||
case PcOpGreater:
|
||||
t.Greater()
|
||||
case PcOpLessEq:
|
||||
t.LessEqual()
|
||||
case PcOpGreaterEq:
|
||||
t.GreaterEqual()
|
||||
case PcOpInString:
|
||||
t.InString()
|
||||
|
||||
// --- Logical ---
|
||||
case PcOpAnd:
|
||||
t.And()
|
||||
case PcOpOr:
|
||||
t.Or()
|
||||
case PcOpNot:
|
||||
t.Not()
|
||||
|
||||
// --- Flow control ---
|
||||
case PcOpJump:
|
||||
offset := int32(binary.LittleEndian.Uint32(code[pc:]))
|
||||
pc += 4
|
||||
pc += int(offset)
|
||||
case PcOpJumpFalse:
|
||||
offset := int32(binary.LittleEndian.Uint32(code[pc:]))
|
||||
pc += 4
|
||||
if !t.PopLogical() {
|
||||
pc += int(offset)
|
||||
}
|
||||
case PcOpJumpTrue:
|
||||
offset := int32(binary.LittleEndian.Uint32(code[pc:]))
|
||||
pc += 4
|
||||
if t.PopLogical() {
|
||||
pc += int(offset)
|
||||
}
|
||||
case PcOpReturn:
|
||||
return
|
||||
case PcOpRetValue:
|
||||
t.RetValue()
|
||||
return
|
||||
|
||||
// --- Frame ---
|
||||
case PcOpFrame:
|
||||
// Already called at function entry; skip if re-encountered
|
||||
pc += 4 // params + locals
|
||||
case PcOpEndProc:
|
||||
return
|
||||
|
||||
// --- Function calls ---
|
||||
case PcOpPushSymbol:
|
||||
slen := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
name := string(code[pc : pc+slen])
|
||||
pc += slen
|
||||
sym := t.VM().FindSymbol(name)
|
||||
t.PushSymbol(sym)
|
||||
case PcOpPushNilArg:
|
||||
t.PushNil()
|
||||
case PcOpFunction:
|
||||
nArgs := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.Function(nArgs)
|
||||
case PcOpDo:
|
||||
nArgs := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.Do(nArgs)
|
||||
|
||||
// --- Self / OOP ---
|
||||
case PcOpPushSelf:
|
||||
t.PushSelf()
|
||||
case PcOpPushSelfField:
|
||||
slen := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
name := string(code[pc : pc+slen])
|
||||
pc += slen
|
||||
t.PushSelfField(name)
|
||||
case PcOpSetSelfField:
|
||||
slen := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
name := string(code[pc : pc+slen])
|
||||
pc += slen
|
||||
t.SetSelfField(name)
|
||||
case PcOpSend:
|
||||
slen := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
name := string(code[pc : pc+slen])
|
||||
pc += slen
|
||||
nArgs := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.Send(name, nArgs)
|
||||
|
||||
// --- Array ---
|
||||
case PcOpArrayGen:
|
||||
count := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
t.ArrayGen(count)
|
||||
case PcOpArrayPush:
|
||||
t.ArrayPush()
|
||||
case PcOpArrayPop:
|
||||
t.ArrayPop()
|
||||
|
||||
// --- Block ---
|
||||
case PcOpPushBlock:
|
||||
codeLen := int(binary.LittleEndian.Uint32(code[pc:]))
|
||||
pc += 4
|
||||
blockCode := make([]byte, codeLen)
|
||||
copy(blockCode, code[pc:pc+codeLen])
|
||||
pc += codeLen
|
||||
nDetached := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
|
||||
// Create a Go function that interprets the block's pcode
|
||||
blockFn := &PcodeFunc{Code: blockCode}
|
||||
modCopy := mod
|
||||
t.PushBlock(func(t2 *Thread) {
|
||||
ExecPcode(t2, blockFn, modCopy)
|
||||
}, nDetached)
|
||||
|
||||
// --- Local ops ---
|
||||
case PcOpLocalAddInt:
|
||||
idx := int(binary.LittleEndian.Uint16(code[pc:]))
|
||||
pc += 2
|
||||
val := int32(binary.LittleEndian.Uint32(code[pc:]))
|
||||
pc += 4
|
||||
t.LocalAddInt(idx, int64(val))
|
||||
case PcOpInc:
|
||||
t.Inc()
|
||||
case PcOpDec:
|
||||
t.Dec()
|
||||
|
||||
case PcOpPopLogical:
|
||||
t.PopLogical()
|
||||
|
||||
case PcOpLine:
|
||||
pc += 2 // skip line number (for debugging)
|
||||
|
||||
case PcOpHalt:
|
||||
return
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown pcode opcode: 0x%02X at pc=%d", op, pc-1))
|
||||
}
|
||||
}
|
||||
}
|
||||
114
hbrt/pcode.go
Normal file
114
hbrt/pcode.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Five pcode — stack-based bytecode for FRB interpreter mode.
|
||||
// Each opcode maps 1:1 to a Thread method call, making the pcode
|
||||
// a direct serialization of what gengo generates as Go code.
|
||||
//
|
||||
// Format: [opcode:1byte] [operands:variable]
|
||||
// Strings: [len:uint16 LE] [bytes]
|
||||
// Numbers: int64 = 8 bytes LE, float64 = 8 bytes LE
|
||||
|
||||
package hbrt
|
||||
|
||||
// Opcode definitions
|
||||
const (
|
||||
// Stack operations
|
||||
PcOpNop byte = 0x00
|
||||
PcOpPushNil byte = 0x01
|
||||
PcOpPushTrue byte = 0x02
|
||||
PcOpPushFalse byte = 0x03
|
||||
PcOpPushInt byte = 0x04 // + int64 LE
|
||||
PcOpPushDouble byte = 0x05 // + float64 LE (8 bytes)
|
||||
PcOpPushString byte = 0x06 // + uint16 len + bytes
|
||||
PcOpPushLocal byte = 0x07 // + uint16 index
|
||||
PcOpPopLocal byte = 0x08 // + uint16 index
|
||||
PcOpPop byte = 0x09
|
||||
PcOpDup byte = 0x0A
|
||||
|
||||
// Arithmetic
|
||||
PcOpPlus byte = 0x10
|
||||
PcOpMinus byte = 0x11
|
||||
PcOpMult byte = 0x12
|
||||
PcOpDivide byte = 0x13
|
||||
PcOpMod byte = 0x14
|
||||
PcOpPower byte = 0x15
|
||||
PcOpNegate byte = 0x16
|
||||
|
||||
// Comparison
|
||||
PcOpEqual byte = 0x20
|
||||
PcOpNotEqual byte = 0x21
|
||||
PcOpLess byte = 0x22
|
||||
PcOpGreater byte = 0x23
|
||||
PcOpLessEq byte = 0x24
|
||||
PcOpGreaterEq byte = 0x25
|
||||
PcOpInString byte = 0x26
|
||||
|
||||
// Logical
|
||||
PcOpAnd byte = 0x28
|
||||
PcOpOr byte = 0x29
|
||||
PcOpNot byte = 0x2A
|
||||
|
||||
// String
|
||||
PcOpConcat byte = 0x2C // same as Plus for strings
|
||||
|
||||
// Flow control
|
||||
PcOpJump byte = 0x30 // + int32 LE (relative offset)
|
||||
PcOpJumpFalse byte = 0x31 // + int32 LE
|
||||
PcOpJumpTrue byte = 0x32 // + int32 LE
|
||||
PcOpReturn byte = 0x33
|
||||
PcOpRetValue byte = 0x34
|
||||
|
||||
// Frame
|
||||
PcOpFrame byte = 0x38 // + uint16 params + uint16 locals
|
||||
PcOpEndProc byte = 0x39
|
||||
|
||||
// Function calls
|
||||
PcOpPushSymbol byte = 0x40 // + uint16 string len + name
|
||||
PcOpPushNilArg byte = 0x41 // push NIL for function self
|
||||
PcOpFunction byte = 0x42 // + uint16 nArgs
|
||||
PcOpDo byte = 0x43 // + uint16 nArgs
|
||||
|
||||
// Self / OOP
|
||||
PcOpPushSelf byte = 0x48
|
||||
PcOpPushSelfField byte = 0x49 // + uint16 len + name
|
||||
PcOpSetSelfField byte = 0x4A // + uint16 len + name
|
||||
PcOpSend byte = 0x4B // + uint16 len + name + uint16 nArgs
|
||||
|
||||
// Array / Hash
|
||||
PcOpArrayGen byte = 0x50 // + uint16 count
|
||||
PcOpHashGen byte = 0x51 // + uint16 count
|
||||
PcOpArrayPush byte = 0x52
|
||||
PcOpArrayPop byte = 0x53
|
||||
|
||||
// Block
|
||||
PcOpPushBlock byte = 0x58 // + uint32 codeLen + pcode bytes + uint16 nDetached
|
||||
|
||||
// Local operations
|
||||
PcOpLocalAddInt byte = 0x60 // + uint16 index + int32 value
|
||||
PcOpInc byte = 0x61
|
||||
PcOpDec byte = 0x62
|
||||
|
||||
// Special
|
||||
PcOpPopLogical byte = 0x70 // pop and store logical result
|
||||
PcOpPushBool byte = 0x71 // + 1 byte (0 or 1)
|
||||
|
||||
// Line info (for debugging)
|
||||
PcOpLine byte = 0xFE // + uint16 lineNo
|
||||
PcOpHalt byte = 0xFF
|
||||
)
|
||||
|
||||
// PcodeFunc represents a pcode-compiled function.
|
||||
type PcodeFunc struct {
|
||||
Name string
|
||||
Code []byte // bytecode
|
||||
Params int // number of parameters
|
||||
Locals int // number of locals
|
||||
}
|
||||
|
||||
// PcodeModule represents a compiled pcode module (multiple functions).
|
||||
type PcodeModule struct {
|
||||
Name string
|
||||
Funcs map[string]*PcodeFunc
|
||||
Strings []string // string constant pool
|
||||
}
|
||||
118
hbrt/pcserial.go
Normal file
118
hbrt/pcserial.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Pcode serialization/deserialization for FRB files.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SerializePcodeModule writes a PcodeModule to bytes.
|
||||
// Format:
|
||||
// uint16 funcCount
|
||||
// for each func:
|
||||
// uint16 nameLen + name
|
||||
// uint16 params
|
||||
// uint16 locals
|
||||
// uint32 codeLen + code
|
||||
func SerializePcodeModule(mod *PcodeModule) []byte {
|
||||
var buf []byte
|
||||
|
||||
// Function count
|
||||
var tmp [2]byte
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(len(mod.Funcs)))
|
||||
buf = append(buf, tmp[:]...)
|
||||
|
||||
for name, fn := range mod.Funcs {
|
||||
// Name
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(len(name)))
|
||||
buf = append(buf, tmp[:]...)
|
||||
buf = append(buf, []byte(name)...)
|
||||
|
||||
// Params + Locals
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(fn.Params))
|
||||
buf = append(buf, tmp[:]...)
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(fn.Locals))
|
||||
buf = append(buf, tmp[:]...)
|
||||
|
||||
// Code
|
||||
var tmp4 [4]byte
|
||||
binary.LittleEndian.PutUint32(tmp4[:], uint32(len(fn.Code)))
|
||||
buf = append(buf, tmp4[:]...)
|
||||
buf = append(buf, fn.Code...)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// DeserializePcodeModule reads a PcodeModule from bytes.
|
||||
func DeserializePcodeModule(data []byte) (*PcodeModule, error) {
|
||||
if len(data) < 2 {
|
||||
return nil, fmt.Errorf("pcode data too short")
|
||||
}
|
||||
|
||||
mod := &PcodeModule{
|
||||
Funcs: make(map[string]*PcodeFunc),
|
||||
}
|
||||
|
||||
pos := 0
|
||||
funcCount := int(binary.LittleEndian.Uint16(data[pos:]))
|
||||
pos += 2
|
||||
|
||||
for i := 0; i < funcCount; i++ {
|
||||
if pos+2 > len(data) {
|
||||
return nil, fmt.Errorf("truncated pcode at func %d", i)
|
||||
}
|
||||
|
||||
// Name
|
||||
nameLen := int(binary.LittleEndian.Uint16(data[pos:]))
|
||||
pos += 2
|
||||
name := string(data[pos : pos+nameLen])
|
||||
pos += nameLen
|
||||
|
||||
// Params + Locals
|
||||
params := int(binary.LittleEndian.Uint16(data[pos:]))
|
||||
pos += 2
|
||||
locals := int(binary.LittleEndian.Uint16(data[pos:]))
|
||||
pos += 2
|
||||
|
||||
// Code
|
||||
codeLen := int(binary.LittleEndian.Uint32(data[pos:]))
|
||||
pos += 4
|
||||
code := make([]byte, codeLen)
|
||||
copy(code, data[pos:pos+codeLen])
|
||||
pos += codeLen
|
||||
|
||||
mod.Funcs[name] = &PcodeFunc{
|
||||
Name: name,
|
||||
Code: code,
|
||||
Params: params,
|
||||
Locals: locals,
|
||||
}
|
||||
}
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
// SymDef is a helper for creating modules from pcode.
|
||||
type SymDef struct {
|
||||
Name string
|
||||
Scope uint16
|
||||
Fn func(*Thread)
|
||||
}
|
||||
|
||||
// NewModuleFromDefs creates a Module from SymDef slice.
|
||||
func NewModuleFromDefs(name string, defs []SymDef) *Module {
|
||||
syms := make([]Symbol, len(defs))
|
||||
for i, d := range defs {
|
||||
syms[i] = Symbol{
|
||||
Name: d.Name,
|
||||
Scope: d.Scope,
|
||||
Func: d.Fn,
|
||||
}
|
||||
}
|
||||
return &Module{Name: name, Symbols: syms}
|
||||
}
|
||||
58
hbrt/symbol.go
Normal file
58
hbrt/symbol.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
// Scope flags (matching Harbour's HB_FS_*)
|
||||
const (
|
||||
FsPublic uint16 = 0x0001
|
||||
FsStatic uint16 = 0x0002
|
||||
FsFirst uint16 = 0x0004
|
||||
FsInit uint16 = 0x0008
|
||||
FsExit uint16 = 0x0010
|
||||
FsMessage uint16 = 0x0020
|
||||
FsMemvar uint16 = 0x0080
|
||||
FsPcodeFunc uint16 = 0x0100
|
||||
FsLocal uint16 = 0x0200
|
||||
FsDynCode uint16 = 0x0400
|
||||
FsDeferred uint16 = 0x0800
|
||||
FsFrame uint16 = 0x1000
|
||||
)
|
||||
|
||||
// Symbol represents a function/variable symbol.
|
||||
type Symbol struct {
|
||||
Name string
|
||||
Scope uint16
|
||||
Func func(*Thread) // nil for external/deferred
|
||||
}
|
||||
|
||||
// Module is a collection of symbols from one PRG file.
|
||||
type Module struct {
|
||||
Name string
|
||||
Symbols []Symbol
|
||||
}
|
||||
|
||||
// Sym creates a Symbol (convenience constructor for generated code).
|
||||
func Sym(name string, scope uint16, fn func(*Thread)) Symbol {
|
||||
return Symbol{Name: name, Scope: scope, Func: fn}
|
||||
}
|
||||
|
||||
// NewModule creates a Module with the given symbols.
|
||||
func NewModule(name string, symbols ...Symbol) *Module {
|
||||
return &Module{Name: name, Symbols: symbols}
|
||||
}
|
||||
|
||||
// At returns a pointer to the symbol at index (for generated code).
|
||||
func (m *Module) At(index int) *Symbol {
|
||||
return &m.Symbols[index]
|
||||
}
|
||||
|
||||
// Find returns a symbol by name within this module.
|
||||
func (m *Module) Find(name string) *Symbol {
|
||||
for i := range m.Symbols {
|
||||
if m.Symbols[i].Name == name {
|
||||
return &m.Symbols[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
483
hbrt/thread.go
Normal file
483
hbrt/thread.go
Normal file
@@ -0,0 +1,483 @@
|
||||
// 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()
|
||||
}
|
||||
276
hbrt/thread_test.go
Normal file
276
hbrt/thread_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import "testing"
|
||||
|
||||
func newTestThread() *Thread {
|
||||
vm := NewVM()
|
||||
return vm.NewThread()
|
||||
}
|
||||
|
||||
// --- Stack operations ---
|
||||
|
||||
func TestStackPushPop(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.push(MakeInt(10))
|
||||
th.push(MakeInt(20))
|
||||
th.push(MakeInt(30))
|
||||
|
||||
if th.sp != 3 {
|
||||
t.Fatalf("sp = %d, want 3", th.sp)
|
||||
}
|
||||
|
||||
v := th.pop()
|
||||
if v.AsInt() != 30 {
|
||||
t.Errorf("pop = %d, want 30", v.AsInt())
|
||||
}
|
||||
v = th.pop()
|
||||
if v.AsInt() != 20 {
|
||||
t.Errorf("pop = %d, want 20", v.AsInt())
|
||||
}
|
||||
v = th.pop()
|
||||
if v.AsInt() != 10 {
|
||||
t.Errorf("pop = %d, want 10", v.AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackPeek(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.push(MakeInt(42))
|
||||
v := th.peek()
|
||||
if v.AsInt() != 42 {
|
||||
t.Errorf("peek = %d, want 42", v.AsInt())
|
||||
}
|
||||
if th.sp != 1 {
|
||||
t.Error("peek should not change sp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackDup(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.PushInt(99)
|
||||
th.Dup()
|
||||
if th.sp != 2 {
|
||||
t.Fatalf("sp = %d, want 2", th.sp)
|
||||
}
|
||||
a := th.pop()
|
||||
b := th.pop()
|
||||
if a.AsInt() != 99 || b.AsInt() != 99 {
|
||||
t.Error("Dup should duplicate top")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Frame and locals ---
|
||||
|
||||
func TestFrameLocals(t *testing.T) {
|
||||
th := newTestThread()
|
||||
|
||||
// Simulate: FUNCTION Foo(a, b) with LOCAL c
|
||||
th.push(MakeInt(10)) // arg a
|
||||
th.push(MakeInt(20)) // arg b
|
||||
th.PendingParams2(2) // tell Frame how many args are on stack
|
||||
th.Frame(2, 1) // 2 params, 1 local
|
||||
|
||||
// Param a = local 1
|
||||
if th.Local(1).AsInt() != 10 {
|
||||
t.Errorf("local 1 = %d, want 10", th.Local(1).AsInt())
|
||||
}
|
||||
// Param b = local 2
|
||||
if th.Local(2).AsInt() != 20 {
|
||||
t.Errorf("local 2 = %d, want 20", th.Local(2).AsInt())
|
||||
}
|
||||
// Local c = local 3 (NIL)
|
||||
if !th.Local(3).IsNil() {
|
||||
t.Error("local 3 should be NIL")
|
||||
}
|
||||
|
||||
// Set local 3
|
||||
th.SetLocal(3, MakeString("hello"))
|
||||
if th.Local(3).AsString() != "hello" {
|
||||
t.Error("local 3 should be 'hello'")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestLocalSetInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 2)
|
||||
|
||||
th.LocalSetInt(1, 42)
|
||||
th.LocalSetInt(2, -99)
|
||||
|
||||
if th.Local(1).AsInt() != 42 {
|
||||
t.Errorf("local 1 = %d, want 42", th.Local(1).AsInt())
|
||||
}
|
||||
if th.Local(2).AsInt() != -99 {
|
||||
t.Errorf("local 2 = %d, want -99", th.Local(2).AsInt())
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestPushPopLocal(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 2)
|
||||
|
||||
th.LocalSetInt(1, 100)
|
||||
th.PushLocal(1)
|
||||
|
||||
if th.peek().AsInt() != 100 {
|
||||
t.Error("PushLocal should push local value")
|
||||
}
|
||||
|
||||
th.PushInt(200)
|
||||
th.PopLocal(2)
|
||||
|
||||
if th.Local(2).AsInt() != 200 {
|
||||
t.Error("PopLocal should set local from stack")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Return value ---
|
||||
|
||||
func TestRetValue(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushInt(42)
|
||||
th.RetValue()
|
||||
|
||||
if th.GetRetValue().AsInt() != 42 {
|
||||
t.Errorf("RetValue = %d, want 42", th.GetRetValue().AsInt())
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestRetInt(t *testing.T) {
|
||||
th := newTestThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.RetInt(999)
|
||||
if th.GetRetValue().AsLong() != 999 {
|
||||
t.Errorf("RetInt = %d, want 999", th.GetRetValue().AsLong())
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Function call ---
|
||||
|
||||
func TestFunctionCall(t *testing.T) {
|
||||
vm := NewVM()
|
||||
|
||||
// Register a simple function: FUNCTION Double(n) → n * 2
|
||||
mod := NewModule("TEST",
|
||||
Sym("DOUBLE", FsPublic|FsLocal, func(th *Thread) {
|
||||
th.Frame(1, 0)
|
||||
defer th.EndProc()
|
||||
n := th.Local(1).AsNumInt()
|
||||
th.RetInt(n * 2)
|
||||
}),
|
||||
)
|
||||
vm.RegisterModule(mod)
|
||||
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
|
||||
// Call: Double(21)
|
||||
th.PushSymbol(mod.At(0))
|
||||
th.PushNil()
|
||||
th.PushInt(21)
|
||||
th.Function(1)
|
||||
|
||||
result := th.pop()
|
||||
if result.AsLong() != 42 {
|
||||
t.Errorf("Double(21) = %d, want 42", result.AsLong())
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
func TestNestedFunctionCall(t *testing.T) {
|
||||
vm := NewVM()
|
||||
|
||||
// FUNCTION Add(a, b) → a + b (simplified)
|
||||
addSym := Sym("ADD", FsPublic|FsLocal, func(th *Thread) {
|
||||
th.Frame(2, 0)
|
||||
defer th.EndProc()
|
||||
a := th.Local(1).AsNumInt()
|
||||
b := th.Local(2).AsNumInt()
|
||||
th.RetInt(a + b)
|
||||
})
|
||||
|
||||
// FUNCTION Main() → Add(10, Add(20, 30))
|
||||
mainSym := Sym("MAIN", FsPublic|FsLocal|FsFirst, func(th *Thread) {
|
||||
th.Frame(0, 0)
|
||||
defer th.EndProc()
|
||||
|
||||
// Inner call: Add(20, 30)
|
||||
th.PushSymbol(vm.FindSymbol("ADD"))
|
||||
th.PushNil()
|
||||
th.PushInt(20)
|
||||
th.PushInt(30)
|
||||
th.Function(2) // → 50 on stack
|
||||
|
||||
// Outer call: Add(10, <result>)
|
||||
innerResult := th.pop()
|
||||
th.PushSymbol(vm.FindSymbol("ADD"))
|
||||
th.PushNil()
|
||||
th.PushInt(10)
|
||||
th.PushValue(innerResult)
|
||||
th.Function(2) // → 60 on stack
|
||||
|
||||
th.RetValue()
|
||||
})
|
||||
|
||||
mod := NewModule("TEST", addSym, mainSym)
|
||||
vm.RegisterModule(mod)
|
||||
|
||||
result := vm.Run("MAIN")
|
||||
if result.AsLong() != 60 {
|
||||
t.Errorf("Main() = %d, want 60", result.AsLong())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Static variables ---
|
||||
|
||||
func TestStaticVariables(t *testing.T) {
|
||||
th := newTestThread()
|
||||
statics := []Value{MakeInt(0), MakeString("hello")}
|
||||
th.RegisterStatics("MOD1", statics)
|
||||
|
||||
th.Frame(0, 0)
|
||||
|
||||
th.PushStatic("MOD1", 1)
|
||||
if th.pop().AsInt() != 0 {
|
||||
t.Error("static 1 should be 0")
|
||||
}
|
||||
|
||||
th.PushInt(42)
|
||||
th.PopStatic("MOD1", 1)
|
||||
th.PushStatic("MOD1", 1)
|
||||
if th.pop().AsInt() != 42 {
|
||||
t.Error("static 1 should be 42 after PopStatic")
|
||||
}
|
||||
|
||||
th.EndProc()
|
||||
}
|
||||
|
||||
// --- Panic recovery ---
|
||||
|
||||
func TestStackUnderflowPanic(t *testing.T) {
|
||||
th := newTestThread()
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Error("expected panic on stack underflow")
|
||||
}
|
||||
}()
|
||||
th.pop() // should panic
|
||||
}
|
||||
430
hbrt/value.go
Normal file
430
hbrt/value.go
Normal file
@@ -0,0 +1,430 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
// Package hbrt provides the core runtime for the Five language.
|
||||
// Tagged Value 24B: the fundamental value representation.
|
||||
//
|
||||
// Layout:
|
||||
// scalar (uint64): numeric/date/bool raw bits
|
||||
// info (uint64): [type:8][meta:24][aux:32]
|
||||
// ptr (unsafe.Pointer): GC-traced pointer for string/array/hash/block
|
||||
//
|
||||
// Design rationale:
|
||||
// - 24B vs Harbour's 32B HB_ITEM = 25% smaller
|
||||
// - Scalar types (55% of runtime values) use scalar+info only, ptr=nil
|
||||
// - Pointer types use ptr field, which Go's GC can trace directly
|
||||
// - No global pointer store, no mutex, no memory leaks
|
||||
// - Inspired by typescript-go (tsgo): "don't fight the GC, design around it"
|
||||
//
|
||||
// See docs/harbour-type-system-analysis.md for full analysis.
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Value is the fundamental value type in Five (24 bytes).
|
||||
// Scalar types use scalar+info fields (ptr is nil).
|
||||
// Pointer types use ptr field (GC-traced) + info for metadata.
|
||||
type Value struct {
|
||||
scalar uint64 // numeric/date/bool raw bits
|
||||
info uint64 // [type:8 bits][meta:24 bits][aux:32 bits]
|
||||
ptr unsafe.Pointer // GC-traced pointer (nil for scalar types)
|
||||
}
|
||||
|
||||
// --- Type constants (upper 8 bits of info) ---
|
||||
|
||||
const (
|
||||
tNil byte = 0
|
||||
tLogical byte = 1
|
||||
tInt byte = 2
|
||||
tLong byte = 3
|
||||
tDouble byte = 4
|
||||
tDate byte = 5
|
||||
tTimestamp byte = 6
|
||||
tString byte = 7
|
||||
tArray byte = 8
|
||||
tHash byte = 9
|
||||
tBlock byte = 10
|
||||
tSymbol byte = 11
|
||||
tByref byte = 12
|
||||
tPointer byte = 13
|
||||
tObject byte = 14
|
||||
)
|
||||
|
||||
// info field bit layout
|
||||
const (
|
||||
typeShift = 56
|
||||
metaMask = 0x00FFFFFF00000000
|
||||
metaShift = 32
|
||||
auxMask = 0x00000000FFFFFFFF
|
||||
)
|
||||
|
||||
func makeInfo(typ byte, meta uint32, aux uint32) uint64 {
|
||||
return uint64(typ)<<typeShift | uint64(meta&0x00FFFFFF)<<metaShift | uint64(aux)
|
||||
}
|
||||
|
||||
// --- Type checking ---
|
||||
|
||||
func (v Value) Type() byte { return byte(v.info >> typeShift) }
|
||||
func (v Value) IsNil() bool { return v.Type() == tNil }
|
||||
func (v Value) IsLogical() bool { return v.Type() == tLogical }
|
||||
func (v Value) IsInt() bool { return v.Type() == tInt }
|
||||
func (v Value) IsLong() bool { return v.Type() == tLong }
|
||||
func (v Value) IsDouble() bool { return v.Type() == tDouble }
|
||||
func (v Value) IsDate() bool { return v.Type() == tDate }
|
||||
func (v Value) IsTimestamp() bool { return v.Type() == tTimestamp }
|
||||
func (v Value) IsString() bool { return v.Type() == tString }
|
||||
func (v Value) IsArray() bool { t := v.Type(); return t == tArray || t == tObject }
|
||||
func (v Value) IsHash() bool { return v.Type() == tHash }
|
||||
func (v Value) IsBlock() bool { return v.Type() == tBlock }
|
||||
func (v Value) IsSymbol() bool { return v.Type() == tSymbol }
|
||||
func (v Value) IsByref() bool { return v.Type() == tByref }
|
||||
func (v Value) IsPointer() bool { return v.Type() == tPointer }
|
||||
func (v Value) IsObject() bool { return v.Type() == tObject }
|
||||
|
||||
// Composite type checks (matching Harbour's HB_IT_* groups)
|
||||
func (v Value) IsNumeric() bool { t := v.Type(); return t == tInt || t == tLong || t == tDouble }
|
||||
func (v Value) IsNumInt() bool { t := v.Type(); return t == tInt || t == tLong }
|
||||
func (v Value) IsDateTime() bool { t := v.Type(); return t == tDate || t == tTimestamp }
|
||||
|
||||
// --- Scalar constructors (no heap allocation) ---
|
||||
|
||||
func MakeNil() Value {
|
||||
return Value{info: makeInfo(tNil, 0, 0)}
|
||||
}
|
||||
|
||||
func MakeBool(b bool) Value {
|
||||
var d uint64
|
||||
if b {
|
||||
d = 1
|
||||
}
|
||||
return Value{scalar: d, info: makeInfo(tLogical, 0, 0)}
|
||||
}
|
||||
|
||||
// MakeInt creates an integer Value with display width.
|
||||
func MakeInt(v int) Value {
|
||||
return Value{
|
||||
scalar: uint64(int64(v)),
|
||||
info: makeInfo(tInt, uint32(intExpLen(int64(v))), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLong creates a 64-bit integer Value.
|
||||
func MakeLong(v int64) Value {
|
||||
return Value{
|
||||
scalar: uint64(v),
|
||||
info: makeInfo(tLong, uint32(longExpLen(v)), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeDouble creates a double Value with display width and decimal places.
|
||||
func MakeDouble(v float64, length, decimal uint16) Value {
|
||||
meta := uint32(length)<<8 | uint32(decimal)
|
||||
return Value{
|
||||
scalar: math.Float64bits(v),
|
||||
info: makeInfo(tDouble, meta, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeDoubleAuto creates a double with default display format.
|
||||
func MakeDoubleAuto(v float64) Value {
|
||||
return MakeDouble(v, 255, 255)
|
||||
}
|
||||
|
||||
// MakeDate creates a date Value from Julian day number.
|
||||
func MakeDate(julian int64) Value {
|
||||
return Value{scalar: uint64(julian), info: makeInfo(tDate, 0, 0)}
|
||||
}
|
||||
|
||||
// MakeTimestamp creates a timestamp Value from Julian day + milliseconds.
|
||||
func MakeTimestamp(julian int64, timeMs int32) Value {
|
||||
return Value{
|
||||
scalar: uint64(julian),
|
||||
info: makeInfo(tTimestamp, 0, uint32(timeMs)),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Value extraction ---
|
||||
|
||||
func (v Value) AsBool() bool { return v.scalar != 0 }
|
||||
func (v Value) AsInt() int { return int(int64(v.scalar)) }
|
||||
func (v Value) AsLong() int64 { return int64(v.scalar) }
|
||||
func (v Value) AsDouble() float64 { return math.Float64frombits(v.scalar) }
|
||||
func (v Value) AsJulian() int64 { return int64(v.scalar) }
|
||||
func (v Value) AsTimeMs() int32 { return int32(v.info & auxMask) }
|
||||
func (v Value) AsNumInt() int64 { return int64(v.scalar) }
|
||||
|
||||
// AsNumDouble returns a double value from any numeric type.
|
||||
func (v Value) AsNumDouble() float64 {
|
||||
switch v.Type() {
|
||||
case tInt, tLong:
|
||||
return float64(int64(v.scalar))
|
||||
case tDouble:
|
||||
return math.Float64frombits(v.scalar)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Display metadata
|
||||
func (v Value) Length() uint16 {
|
||||
switch v.Type() {
|
||||
case tInt, tLong:
|
||||
return uint16((v.info & metaMask) >> metaShift)
|
||||
case tDouble:
|
||||
return uint16((v.info & metaMask) >> (metaShift + 8))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (v Value) Decimal() uint16 {
|
||||
if v.Type() == tDouble {
|
||||
return uint16((v.info & metaMask) >> metaShift & 0xFF)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- Pointer type backing stores ---
|
||||
|
||||
// HbString is the string backing store.
|
||||
type HbString struct {
|
||||
Data string // Go immutable string (primary storage)
|
||||
Bytes []byte // mutable buffer (for in-place edits, nil if immutable)
|
||||
}
|
||||
|
||||
// HbArray is the array/object backing store.
|
||||
type HbArray struct {
|
||||
Items []Value
|
||||
Class uint16
|
||||
PrevCls uint16
|
||||
}
|
||||
|
||||
// HbHash is the hash table backing store.
|
||||
type HbHash struct {
|
||||
Keys []Value
|
||||
Values []Value
|
||||
Order []int
|
||||
Flags int32
|
||||
}
|
||||
|
||||
// HbBlock is the code block backing store.
|
||||
type HbBlock struct {
|
||||
Fn func(*Thread)
|
||||
DetachedLen int
|
||||
Detached []Value
|
||||
}
|
||||
|
||||
// --- Pointer type constructors ---
|
||||
// These store Go pointers in Value.ptr, which the GC can trace.
|
||||
// No global store, no mutex, no memory leaks.
|
||||
|
||||
// MakeString creates a string Value.
|
||||
func MakeString(s string) Value {
|
||||
hs := &HbString{Data: s}
|
||||
return Value{
|
||||
info: makeInfo(tString, 0, uint32(len(s))),
|
||||
ptr: unsafe.Pointer(hs),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeArray creates an array Value.
|
||||
func MakeArray(size int) Value {
|
||||
ha := &HbArray{Items: make([]Value, size)}
|
||||
return Value{
|
||||
info: makeInfo(tArray, 0, 0),
|
||||
ptr: unsafe.Pointer(ha),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeArrayFrom creates an array Value from existing items.
|
||||
func MakeArrayFrom(items []Value) Value {
|
||||
ha := &HbArray{Items: items}
|
||||
return Value{
|
||||
info: makeInfo(tArray, 0, 0),
|
||||
ptr: unsafe.Pointer(ha),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeObject creates an object Value (array with class).
|
||||
func MakeObject(classID uint16, fieldCount int) Value {
|
||||
ha := &HbArray{Items: make([]Value, fieldCount), Class: classID}
|
||||
return Value{
|
||||
info: makeInfo(tObject, uint32(classID), 0),
|
||||
ptr: unsafe.Pointer(ha),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeHash creates an empty hash Value.
|
||||
func MakeHash() Value {
|
||||
hh := &HbHash{}
|
||||
return Value{
|
||||
info: makeInfo(tHash, 0, 0),
|
||||
ptr: unsafe.Pointer(hh),
|
||||
}
|
||||
}
|
||||
|
||||
func MakeHashFrom(hh *HbHash) Value {
|
||||
return Value{
|
||||
info: makeInfo(tHash, 0, 0),
|
||||
ptr: unsafe.Pointer(hh),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeBlock creates a code block Value.
|
||||
func MakeBlock(fn func(*Thread), detachedLocals int) Value {
|
||||
hb := &HbBlock{
|
||||
Fn: fn,
|
||||
DetachedLen: detachedLocals,
|
||||
Detached: make([]Value, detachedLocals),
|
||||
}
|
||||
return Value{
|
||||
info: makeInfo(tBlock, 0, 0),
|
||||
ptr: unsafe.Pointer(hb),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pointer type accessors ---
|
||||
|
||||
func (v Value) AsString() string {
|
||||
if v.ptr == nil {
|
||||
return ""
|
||||
}
|
||||
hs := (*HbString)(v.ptr)
|
||||
if hs.Bytes != nil {
|
||||
return string(hs.Bytes)
|
||||
}
|
||||
return hs.Data
|
||||
}
|
||||
|
||||
func (v Value) StringLen() int {
|
||||
return int(v.info & auxMask)
|
||||
}
|
||||
|
||||
func (v Value) AsArray() *HbArray {
|
||||
if v.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return (*HbArray)(v.ptr)
|
||||
}
|
||||
|
||||
func (v Value) AsHash() *HbHash {
|
||||
if v.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return (*HbHash)(v.ptr)
|
||||
}
|
||||
|
||||
func (v Value) AsBlock() *HbBlock {
|
||||
if v.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return (*HbBlock)(v.ptr)
|
||||
}
|
||||
|
||||
// AsPointer returns the Go interface{} stored in a Pointer value.
|
||||
func (v Value) AsPointer() interface{} {
|
||||
if v.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return *(*interface{})(v.ptr)
|
||||
}
|
||||
|
||||
// MakePointer wraps an arbitrary Go value as a Harbour Pointer type.
|
||||
func MakePointer(val interface{}) Value {
|
||||
p := new(interface{})
|
||||
*p = val
|
||||
return Value{
|
||||
info: makeInfo(tPointer, 0, 0),
|
||||
ptr: unsafe.Pointer(p),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Numeric auto-promotion ---
|
||||
|
||||
// MakeNumInt creates an Int or Long depending on value range.
|
||||
func MakeNumInt(v int64) Value {
|
||||
if v >= math.MinInt32 && v <= math.MaxInt32 {
|
||||
return MakeInt(int(v))
|
||||
}
|
||||
return MakeLong(v)
|
||||
}
|
||||
|
||||
// --- Display length helpers ---
|
||||
|
||||
func intExpLen(v int64) int {
|
||||
if v == 0 {
|
||||
return 1
|
||||
}
|
||||
n := 0
|
||||
if v < 0 {
|
||||
n = 1
|
||||
v = -v
|
||||
}
|
||||
for v > 0 {
|
||||
n++
|
||||
v /= 10
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func longExpLen(v int64) int {
|
||||
return intExpLen(v)
|
||||
}
|
||||
|
||||
// --- Stringer ---
|
||||
|
||||
func (v Value) String() string {
|
||||
switch v.Type() {
|
||||
case tNil:
|
||||
return "NIL"
|
||||
case tLogical:
|
||||
if v.AsBool() {
|
||||
return ".T."
|
||||
}
|
||||
return ".F."
|
||||
case tInt:
|
||||
return fmt.Sprintf("%d", v.AsInt())
|
||||
case tLong:
|
||||
return fmt.Sprintf("%d", v.AsLong())
|
||||
case tDouble:
|
||||
return fmt.Sprintf("%g", v.AsDouble())
|
||||
case tDate:
|
||||
return fmt.Sprintf("Date(%d)", v.AsJulian())
|
||||
case tTimestamp:
|
||||
return fmt.Sprintf("Timestamp(%d,%d)", v.AsJulian(), v.AsTimeMs())
|
||||
case tString:
|
||||
return fmt.Sprintf("%q", v.AsString())
|
||||
case tArray:
|
||||
arr := v.AsArray()
|
||||
if arr == nil {
|
||||
return "Array(nil)"
|
||||
}
|
||||
return fmt.Sprintf("Array(%d)", len(arr.Items))
|
||||
case tObject:
|
||||
arr := v.AsArray()
|
||||
if arr == nil {
|
||||
return "Object(nil)"
|
||||
}
|
||||
return fmt.Sprintf("Object(class=%d, fields=%d)", arr.Class, len(arr.Items))
|
||||
case tHash:
|
||||
hh := v.AsHash()
|
||||
if hh == nil {
|
||||
return "Hash(nil)"
|
||||
}
|
||||
return fmt.Sprintf("Hash(%d)", len(hh.Keys))
|
||||
case tBlock:
|
||||
return "Block{...}"
|
||||
case tSymbol:
|
||||
return "Symbol"
|
||||
case tByref:
|
||||
return "Byref"
|
||||
case tPointer:
|
||||
return fmt.Sprintf("Pointer(%x)", v.scalar)
|
||||
default:
|
||||
return fmt.Sprintf("Unknown(type=%d)", v.Type())
|
||||
}
|
||||
}
|
||||
380
hbrt/value_test.go
Normal file
380
hbrt/value_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestValueSize(t *testing.T) {
|
||||
if size := unsafe.Sizeof(Value{}); size != 24 {
|
||||
t.Errorf("sizeof(Value) = %d, want 24", size)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Nil ---
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
v := MakeNil()
|
||||
if !v.IsNil() {
|
||||
t.Error("MakeNil().IsNil() should be true")
|
||||
}
|
||||
if v.Type() != tNil {
|
||||
t.Errorf("MakeNil().Type() = %d, want %d", v.Type(), tNil)
|
||||
}
|
||||
if v.IsNumeric() || v.IsString() || v.IsLogical() {
|
||||
t.Error("Nil should not match other types")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logical ---
|
||||
|
||||
func TestLogical(t *testing.T) {
|
||||
vt := MakeBool(true)
|
||||
vf := MakeBool(false)
|
||||
|
||||
if !vt.IsLogical() {
|
||||
t.Error("MakeBool(true).IsLogical() should be true")
|
||||
}
|
||||
if !vt.AsBool() {
|
||||
t.Error("MakeBool(true).AsBool() should be true")
|
||||
}
|
||||
if vf.AsBool() {
|
||||
t.Error("MakeBool(false).AsBool() should be false")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Integer ---
|
||||
|
||||
func TestInt(t *testing.T) {
|
||||
tests := []int{0, 1, -1, 42, -42, math.MaxInt32, math.MinInt32}
|
||||
for _, n := range tests {
|
||||
v := MakeInt(n)
|
||||
if !v.IsInt() {
|
||||
t.Errorf("MakeInt(%d).IsInt() should be true", n)
|
||||
}
|
||||
if !v.IsNumeric() {
|
||||
t.Errorf("MakeInt(%d).IsNumeric() should be true", n)
|
||||
}
|
||||
if !v.IsNumInt() {
|
||||
t.Errorf("MakeInt(%d).IsNumInt() should be true", n)
|
||||
}
|
||||
if v.AsInt() != n {
|
||||
t.Errorf("MakeInt(%d).AsInt() = %d", n, v.AsInt())
|
||||
}
|
||||
if v.AsNumInt() != int64(n) {
|
||||
t.Errorf("MakeInt(%d).AsNumInt() = %d", n, v.AsNumInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Long ---
|
||||
|
||||
func TestLong(t *testing.T) {
|
||||
tests := []int64{0, 1, -1, math.MaxInt64, math.MinInt64, 9223372036854775807, -9223372036854775808}
|
||||
for _, n := range tests {
|
||||
v := MakeLong(n)
|
||||
if !v.IsLong() {
|
||||
t.Errorf("MakeLong(%d).IsLong() should be true", n)
|
||||
}
|
||||
if !v.IsNumeric() {
|
||||
t.Errorf("MakeLong(%d).IsNumeric() should be true", n)
|
||||
}
|
||||
if v.AsLong() != n {
|
||||
t.Errorf("MakeLong(%d).AsLong() = %d", n, v.AsLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Double ---
|
||||
|
||||
func TestDouble(t *testing.T) {
|
||||
tests := []struct {
|
||||
val float64
|
||||
length uint16
|
||||
decimal uint16
|
||||
}{
|
||||
{0.0, 1, 0},
|
||||
{3.14, 4, 2},
|
||||
{-123.456, 7, 3},
|
||||
{math.MaxFloat64, 255, 255},
|
||||
{math.SmallestNonzeroFloat64, 255, 255},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
v := MakeDouble(tt.val, tt.length, tt.decimal)
|
||||
if !v.IsDouble() {
|
||||
t.Errorf("MakeDouble(%g).IsDouble() should be true", tt.val)
|
||||
}
|
||||
if !v.IsNumeric() {
|
||||
t.Errorf("MakeDouble(%g).IsNumeric() should be true", tt.val)
|
||||
}
|
||||
if v.AsDouble() != tt.val {
|
||||
t.Errorf("MakeDouble(%g).AsDouble() = %g", tt.val, v.AsDouble())
|
||||
}
|
||||
if v.Length() != tt.length {
|
||||
t.Errorf("MakeDouble(%g).Length() = %d, want %d", tt.val, v.Length(), tt.length)
|
||||
}
|
||||
if v.Decimal() != tt.decimal {
|
||||
t.Errorf("MakeDouble(%g).Decimal() = %d, want %d", tt.val, v.Decimal(), tt.decimal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Date ---
|
||||
|
||||
func TestDate(t *testing.T) {
|
||||
// Julian day for 2026-03-27 ≈ 2461033
|
||||
julian := int64(2461033)
|
||||
v := MakeDate(julian)
|
||||
if !v.IsDate() {
|
||||
t.Error("MakeDate().IsDate() should be true")
|
||||
}
|
||||
if !v.IsDateTime() {
|
||||
t.Error("MakeDate().IsDateTime() should be true")
|
||||
}
|
||||
if v.AsJulian() != julian {
|
||||
t.Errorf("MakeDate().AsJulian() = %d, want %d", v.AsJulian(), julian)
|
||||
}
|
||||
if v.AsTimeMs() != 0 {
|
||||
t.Error("Date should have 0 timeMs")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Timestamp ---
|
||||
|
||||
func TestTimestamp(t *testing.T) {
|
||||
julian := int64(2461033)
|
||||
timeMs := int32(43200000) // 12:00:00.000
|
||||
v := MakeTimestamp(julian, timeMs)
|
||||
if !v.IsTimestamp() {
|
||||
t.Error("MakeTimestamp().IsTimestamp() should be true")
|
||||
}
|
||||
if !v.IsDateTime() {
|
||||
t.Error("MakeTimestamp().IsDateTime() should be true")
|
||||
}
|
||||
if v.AsJulian() != julian {
|
||||
t.Errorf("AsJulian() = %d, want %d", v.AsJulian(), julian)
|
||||
}
|
||||
if v.AsTimeMs() != timeMs {
|
||||
t.Errorf("AsTimeMs() = %d, want %d", v.AsTimeMs(), timeMs)
|
||||
}
|
||||
}
|
||||
|
||||
// --- String ---
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
tests := []string{"", "Hello", "Hello, World!", "한글 테스트", "こんにちは"}
|
||||
for _, s := range tests {
|
||||
v := MakeString(s)
|
||||
if !v.IsString() {
|
||||
t.Errorf("MakeString(%q).IsString() should be true", s)
|
||||
}
|
||||
if v.AsString() != s {
|
||||
t.Errorf("MakeString(%q).AsString() = %q", s, v.AsString())
|
||||
}
|
||||
if v.StringLen() != len(s) {
|
||||
t.Errorf("MakeString(%q).StringLen() = %d, want %d", s, v.StringLen(), len(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Array ---
|
||||
|
||||
func TestArray(t *testing.T) {
|
||||
v := MakeArray(3)
|
||||
if !v.IsArray() {
|
||||
t.Error("MakeArray().IsArray() should be true")
|
||||
}
|
||||
arr := v.AsArray()
|
||||
if arr == nil {
|
||||
t.Fatal("AsArray() should not be nil")
|
||||
}
|
||||
if len(arr.Items) != 3 {
|
||||
t.Errorf("array len = %d, want 3", len(arr.Items))
|
||||
}
|
||||
// All items should be nil initially
|
||||
for i, item := range arr.Items {
|
||||
if !item.IsNil() {
|
||||
t.Errorf("arr[%d] should be Nil, got type %d", i, item.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayFrom(t *testing.T) {
|
||||
items := []Value{MakeInt(1), MakeString("two"), MakeBool(true)}
|
||||
v := MakeArrayFrom(items)
|
||||
arr := v.AsArray()
|
||||
if len(arr.Items) != 3 {
|
||||
t.Fatalf("len = %d, want 3", len(arr.Items))
|
||||
}
|
||||
if arr.Items[0].AsInt() != 1 {
|
||||
t.Error("arr[0] should be 1")
|
||||
}
|
||||
if arr.Items[1].AsString() != "two" {
|
||||
t.Error("arr[1] should be 'two'")
|
||||
}
|
||||
if !arr.Items[2].AsBool() {
|
||||
t.Error("arr[2] should be true")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Object ---
|
||||
|
||||
func TestObject(t *testing.T) {
|
||||
v := MakeObject(1, 5)
|
||||
if !v.IsObject() {
|
||||
t.Error("MakeObject().IsObject() should be true")
|
||||
}
|
||||
if !v.IsArray() {
|
||||
t.Error("Object.IsArray() should be true (object is array with class)")
|
||||
}
|
||||
arr := v.AsArray()
|
||||
if arr.Class != 1 {
|
||||
t.Errorf("Class = %d, want 1", arr.Class)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hash ---
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
v := MakeHash()
|
||||
if !v.IsHash() {
|
||||
t.Error("MakeHash().IsHash() should be true")
|
||||
}
|
||||
hh := v.AsHash()
|
||||
if hh == nil {
|
||||
t.Fatal("AsHash() should not be nil")
|
||||
}
|
||||
if len(hh.Keys) != 0 {
|
||||
t.Error("new hash should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Block ---
|
||||
|
||||
func TestBlock(t *testing.T) {
|
||||
called := false
|
||||
v := MakeBlock(func(t *Thread) {
|
||||
called = true
|
||||
}, 0)
|
||||
if !v.IsBlock() {
|
||||
t.Error("MakeBlock().IsBlock() should be true")
|
||||
}
|
||||
blk := v.AsBlock()
|
||||
if blk == nil {
|
||||
t.Fatal("AsBlock() should not be nil")
|
||||
}
|
||||
blk.Fn(nil) // call it
|
||||
if !called {
|
||||
t.Error("block function should have been called")
|
||||
}
|
||||
}
|
||||
|
||||
// --- NumInt auto-promotion ---
|
||||
|
||||
func TestMakeNumInt(t *testing.T) {
|
||||
// Within int32 range → Int
|
||||
v1 := MakeNumInt(42)
|
||||
if !v1.IsInt() {
|
||||
t.Error("42 should be Int")
|
||||
}
|
||||
|
||||
// Beyond int32 range → Long
|
||||
v2 := MakeNumInt(int64(math.MaxInt32) + 1)
|
||||
if !v2.IsLong() {
|
||||
t.Error("MaxInt32+1 should be Long")
|
||||
}
|
||||
|
||||
v3 := MakeNumInt(int64(math.MinInt32) - 1)
|
||||
if !v3.IsLong() {
|
||||
t.Error("MinInt32-1 should be Long")
|
||||
}
|
||||
}
|
||||
|
||||
// --- AsNumDouble ---
|
||||
|
||||
func TestAsNumDouble(t *testing.T) {
|
||||
if MakeInt(42).AsNumDouble() != 42.0 {
|
||||
t.Error("Int(42).AsNumDouble() should be 42.0")
|
||||
}
|
||||
if MakeLong(1000000000000).AsNumDouble() != 1e12 {
|
||||
t.Error("Long(1e12).AsNumDouble() should be 1e12")
|
||||
}
|
||||
if MakeDouble(3.14, 4, 2).AsNumDouble() != 3.14 {
|
||||
t.Error("Double(3.14).AsNumDouble() should be 3.14")
|
||||
}
|
||||
if MakeNil().AsNumDouble() != 0 {
|
||||
t.Error("Nil.AsNumDouble() should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stringer ---
|
||||
|
||||
func TestStringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
v Value
|
||||
want string
|
||||
}{
|
||||
{MakeNil(), "NIL"},
|
||||
{MakeBool(true), ".T."},
|
||||
{MakeBool(false), ".F."},
|
||||
{MakeInt(42), "42"},
|
||||
{MakeLong(-999), "-999"},
|
||||
{MakeString("hello"), `"hello"`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := tt.v.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("String() = %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Benchmarks ---
|
||||
|
||||
func BenchmarkValueMakeInt(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = MakeInt(i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInterfaceMakeInt(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var v interface{} = int64(i)
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValueAddInt(b *testing.B) {
|
||||
a := MakeInt(100)
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Simulate: read int, add, write back
|
||||
r := a.AsNumInt() + int64(i)
|
||||
a = MakeNumInt(r)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInterfaceAddInt(b *testing.B) {
|
||||
var a interface{} = int64(100)
|
||||
for i := 0; i < b.N; i++ {
|
||||
r := a.(int64) + int64(i)
|
||||
a = r
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValueTypeCheck(b *testing.B) {
|
||||
v := MakeInt(42)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = v.IsNumeric()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInterfaceTypeCheck(b *testing.B) {
|
||||
var v interface{} = int64(42)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = v.(int64)
|
||||
}
|
||||
}
|
||||
161
hbrt/vm.go
Normal file
161
hbrt/vm.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
package hbrt
|
||||
|
||||
import "sync"
|
||||
|
||||
// VM is the shared state across all threads.
|
||||
type VM struct {
|
||||
mu sync.RWMutex
|
||||
modules []*Module
|
||||
symbols map[string]*Symbol
|
||||
statics map[string][]Value
|
||||
waFactory func() interface{} // creates WorkAreaManager for new threads
|
||||
onExit func() // called when Run() finishes (restore terminal etc.)
|
||||
Debugger *Debugger // nil = no debugging; set by five debug command
|
||||
}
|
||||
|
||||
// SetWAFactory sets the factory for creating WorkAreaManagers.
|
||||
func (vm *VM) SetWAFactory(f func() interface{}) {
|
||||
vm.waFactory = f
|
||||
}
|
||||
|
||||
// SetOnExit sets a callback for when Run() finishes.
|
||||
func (vm *VM) SetOnExit(f func()) {
|
||||
vm.onExit = f
|
||||
}
|
||||
|
||||
// Library modules registered via init()
|
||||
var libModules []*Module
|
||||
var dynamicFuncs []Symbol // from HB_FUNC() in #pragma BEGINDUMP
|
||||
|
||||
// RegisterLibModule registers a module from a library PRG file.
|
||||
// Called by init() in generated library code.
|
||||
func RegisterLibModule(m *Module) {
|
||||
libModules = append(libModules, m)
|
||||
}
|
||||
|
||||
// RegisterDynamicFunc registers a Go function callable from PRG.
|
||||
// Called from init() in #pragma BEGINDUMP code via HB_FUNC().
|
||||
func RegisterDynamicFunc(name string, fn func(*Thread)) {
|
||||
dynamicFuncs = append(dynamicFuncs, Symbol{
|
||||
Name: name,
|
||||
Scope: FsPublic | FsLocal,
|
||||
Func: fn,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterLibModules registers any pending lib modules and dynamic functions.
|
||||
func (vm *VM) RegisterLibModules() {
|
||||
for _, m := range libModules {
|
||||
vm.RegisterModule(m)
|
||||
}
|
||||
libModules = nil
|
||||
// Register HB_FUNC dynamic functions from #pragma BEGINDUMP
|
||||
for i := range dynamicFuncs {
|
||||
sym := &dynamicFuncs[i]
|
||||
vm.RegisterSymbol(sym)
|
||||
}
|
||||
dynamicFuncs = nil
|
||||
}
|
||||
|
||||
// NewVM creates a new VM instance.
|
||||
func NewVM() *VM {
|
||||
return &VM{
|
||||
modules: make([]*Module, 0),
|
||||
symbols: make(map[string]*Symbol),
|
||||
statics: make(map[string][]Value),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterModule registers a module's symbols with the VM.
|
||||
func (vm *VM) RegisterModule(m *Module) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
vm.modules = append(vm.modules, m)
|
||||
for i := range m.Symbols {
|
||||
sym := &m.Symbols[i]
|
||||
vm.symbols[sym.Name] = sym
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSymbol registers a single symbol.
|
||||
func (vm *VM) RegisterSymbol(sym *Symbol) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
vm.symbols[sym.Name] = sym
|
||||
}
|
||||
|
||||
// UnregisterSymbol removes a symbol by name. Returns the old symbol if any.
|
||||
func (vm *VM) UnregisterSymbol(name string) *Symbol {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
old := vm.symbols[name]
|
||||
delete(vm.symbols, name)
|
||||
return old
|
||||
}
|
||||
|
||||
// SymbolNames returns all registered symbol names.
|
||||
func (vm *VM) SymbolNames() []string {
|
||||
vm.mu.RLock()
|
||||
defer vm.mu.RUnlock()
|
||||
names := make([]string, 0, len(vm.symbols))
|
||||
for n := range vm.symbols {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// FindSymbol looks up a symbol by name.
|
||||
func (vm *VM) FindSymbol(name string) *Symbol {
|
||||
vm.mu.RLock()
|
||||
defer vm.mu.RUnlock()
|
||||
return vm.symbols[name]
|
||||
}
|
||||
|
||||
// NewThread creates a new Thread attached to this VM.
|
||||
func (vm *VM) NewThread() *Thread {
|
||||
return NewThread(vm)
|
||||
}
|
||||
|
||||
// Run starts execution from the named function.
|
||||
func (vm *VM) Run(funcName string) Value {
|
||||
// Register any library modules from init()
|
||||
for _, m := range libModules {
|
||||
vm.RegisterModule(m)
|
||||
}
|
||||
libModules = nil
|
||||
|
||||
sym := vm.FindSymbol(funcName)
|
||||
if sym == nil {
|
||||
panic("function not found: " + funcName)
|
||||
}
|
||||
if sym.Func == nil {
|
||||
panic("function has no implementation: " + funcName)
|
||||
}
|
||||
|
||||
t := vm.NewThread()
|
||||
|
||||
// Auto-initialize WorkAreaManager if not set
|
||||
if t.WA == nil && vm.waFactory != nil {
|
||||
t.WA = vm.waFactory()
|
||||
}
|
||||
|
||||
// Copy statics to thread
|
||||
vm.mu.RLock()
|
||||
for k, v := range vm.statics {
|
||||
t.statics[k] = v
|
||||
}
|
||||
vm.mu.RUnlock()
|
||||
|
||||
// Call the function, ensure cleanup on exit
|
||||
defer func() {
|
||||
if vm.onExit != nil {
|
||||
vm.onExit()
|
||||
}
|
||||
}()
|
||||
sym.Func(t)
|
||||
|
||||
return t.retVal
|
||||
}
|
||||
Reference in New Issue
Block a user