- 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>
532 lines
14 KiB
Go
532 lines
14 KiB
Go
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)
|
|
}
|
|
}
|