- 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>
277 lines
5.2 KiB
Go
277 lines
5.2 KiB
Go
// 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
|
|
}
|