- 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>
216 lines
4.6 KiB
Go
216 lines
4.6 KiB
Go
// 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")
|
|
}
|
|
}
|