Harbour's ::super: idiom routes a method call through the parent of
the class that defines the currently-executing method — Self stays
the child instance, only the vtable entry point shifts. Five
previously parsed ::super as a data-field access (PushSelfField("SUPER"))
which returned nil and panicked on the subsequent Send.
Runtime: Thread.SendSuper(fromClassName, methodName, nArgs).
Binding to the *defining* class (not Self's runtime class) is
load-bearing for 3+ level hierarchies: without it,
Grand:New → ::super:New → Child:New → ::super:New
would resolve to Grand.Parent=Child again and infinite-loop.
Gengo: Generator.curMethodClass tracks the class name across each
method body emission. emitSendExpr detects the nested SendExpr
shape `::super:X(...)` and emits SendSuper with curMethodClass as
the first argument.
Tested (/tmp/test_super, /tmp/test_super2):
Parent → Child: ::super:Greet() returns composed result
Base → Child → Grand: ::super:New chain passes args correctly
Also fixes three gengo unit tests whose expected output was stale
from prior perf commits (b829ed4 const prop, 1f63c7f symbol hoist,
7e4079f string-concat reassoc) — assertions now match the current
optimized codegen.
FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/parser"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func generate(t *testing.T, source string) string {
|
|
t.Helper()
|
|
file, errs := parser.Parse("test.prg", source)
|
|
if len(errs) > 0 {
|
|
for _, e := range errs {
|
|
t.Errorf("parse error: %s", e)
|
|
}
|
|
t.FailNow()
|
|
}
|
|
return Generate(file)
|
|
}
|
|
|
|
func assertContains(t *testing.T, code, want string) {
|
|
t.Helper()
|
|
if !strings.Contains(code, want) {
|
|
t.Errorf("generated code missing %q\n--- code ---\n%s", want, code)
|
|
}
|
|
}
|
|
|
|
func TestGenerateHelloWorld(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
? "Hello, World!"
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "package main")
|
|
assertContains(t, code, `import (`)
|
|
assertContains(t, code, `"five/hbrt"`)
|
|
assertContains(t, code, "func HB_MAIN(t *hbrt.Thread)")
|
|
assertContains(t, code, "t.Frame(0, 0)")
|
|
assertContains(t, code, "defer t.EndProc()")
|
|
assertContains(t, code, `t.PushString("Hello, World!")`)
|
|
assertContains(t, code, "t.Function(1)")
|
|
assertContains(t, code, "t.PushNil()")
|
|
assertContains(t, code, "t.RetValue()")
|
|
assertContains(t, code, "func main()")
|
|
assertContains(t, code, `vm.Run("MAIN")`)
|
|
}
|
|
|
|
func TestGenerateArithmetic(t *testing.T) {
|
|
// Const prop (b829ed4) inlines `n` as 10 at its read site. The
|
|
// literal fold pass runs before the ident substitution so the
|
|
// outer `10 + 5` doesn't collapse to `15` — leaves two PushInt +
|
|
// Plus. Dead store for `n` is elided (6974ff9).
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
RETURN n + 5
|
|
`)
|
|
assertContains(t, code, "t.Frame(0, 1)")
|
|
assertContains(t, code, "t.PushInt(10)")
|
|
assertContains(t, code, "t.PushInt(5)")
|
|
assertContains(t, code, "t.Plus()")
|
|
assertContains(t, code, "t.RetValue()")
|
|
}
|
|
|
|
func TestGenerateIfElse(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
IF n > 5
|
|
? "Big"
|
|
ELSE
|
|
? "Small"
|
|
ENDIF
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "t.Greater()")
|
|
assertContains(t, code, "if t.PopLogical()")
|
|
assertContains(t, code, `t.PushString("Big")`)
|
|
assertContains(t, code, "} else {")
|
|
assertContains(t, code, `t.PushString("Small")`)
|
|
}
|
|
|
|
func TestGenerateDoWhile(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL i := 0
|
|
DO WHILE i < 10
|
|
i++
|
|
ENDDO
|
|
RETURN i
|
|
`)
|
|
assertContains(t, code, "for {")
|
|
assertContains(t, code, "t.Less()")
|
|
assertContains(t, code, "if !t.PopLogical() { break }")
|
|
assertContains(t, code, "t.LocalAddInt(1, 1)") // i++
|
|
}
|
|
|
|
func TestGenerateForNext(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL i, nSum := 0
|
|
FOR i := 1 TO 10
|
|
nSum += i
|
|
NEXT
|
|
RETURN nSum
|
|
`)
|
|
assertContains(t, code, "t.Frame(0, 2)")
|
|
assertContains(t, code, "for {")
|
|
assertContains(t, code, "LocalLessEqualInt(")
|
|
assertContains(t, code, "t.LocalAdd(") // nSum += i
|
|
assertContains(t, code, "t.LocalAddInt(") // i += 1
|
|
}
|
|
|
|
func TestGenerateMultipleFunctions(t *testing.T) {
|
|
// Symbol hoist (1f63c7f) replaced `t.VM().FindSymbol(...)` with a
|
|
// per-file package-level pointer populated lazily via GetSym.
|
|
code := generate(t, `FUNCTION Double(n)
|
|
RETURN n * 2
|
|
|
|
FUNCTION Main()
|
|
? Double(21)
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "func HB_DOUBLE(t *hbrt.Thread)")
|
|
assertContains(t, code, "func HB_MAIN(t *hbrt.Thread)")
|
|
assertContains(t, code, "t.Frame(1, 0)") // Double has 1 param
|
|
assertContains(t, code, "t.Mult()")
|
|
assertContains(t, code, `t.GetSym(&_sym_test_DOUBLE, "DOUBLE")`)
|
|
}
|
|
|
|
func TestGenerateStringConcat(t *testing.T) {
|
|
// cName propagates to "World" (b829ed4). The string-concat fold
|
|
// (7e4079f) works on literal+literal pairs, which is what the
|
|
// three PushStrings + Plus calls produce.
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL cName := "World"
|
|
? "Hello, " + cName + "!"
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `t.PushString("Hello, ")`)
|
|
assertContains(t, code, `t.PushString("World")`)
|
|
assertContains(t, code, `t.PushString("!")`)
|
|
assertContains(t, code, "t.Plus()")
|
|
}
|
|
|
|
func TestGenerateSymbolTable(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
RETURN NIL
|
|
|
|
FUNCTION Helper()
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `hbrt.Sym("MAIN"`)
|
|
assertContains(t, code, `hbrt.Sym("HELPER"`)
|
|
assertContains(t, code, "hbrt.FsFirst")
|
|
}
|
|
|
|
func TestGenerateImport(t *testing.T) {
|
|
code := generate(t, `IMPORT "net/http"
|
|
|
|
FUNCTION Main()
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `"net/http"`)
|
|
}
|