feat(oop): ::super:Method() dispatch for inheritance chains

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>
This commit is contained in:
2026-04-18 15:33:46 +09:00
parent 3d292dd9d8
commit 3a56bd321a
4 changed files with 87 additions and 5 deletions

View File

@@ -134,12 +134,17 @@ func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
}
g.curLocals = localMap
// Bind defining class for ::super: resolution in emitSendExpr.
prevCls := g.curMethodClass
g.curMethodClass = className
// Emit body
for _, stmt := range md.Body {
g.emitStmt(stmt, localMap)
}
g.curMethodClass = prevCls
g.indent--
g.writeln("}")
g.writeln("")

View File

@@ -49,6 +49,9 @@ type Generator struct {
// Per-function constant locals: LOCAL names (uppercase) whose sole
// assignment is a literal initializer. Reads get substituted inline.
constLocals map[string]*ast.LiteralExpr
// Class name of the currently-emitting method body, used to resolve
// ::super: at compile time against the defining class's Parent.
curMethodClass string
}
type symbolEntry struct {
@@ -3094,6 +3097,26 @@ func (g *Generator) fieldName(expr ast.Expr) string {
}
func (g *Generator) emitSendExpr(e *ast.SendExpr) {
// ::super:Method(args) — dispatch to parent class. The parse tree
// is nested: outer SendExpr.Object is itself a SendExpr whose
// Object is ::SELF and Method is "super". Detect that shape and
// route through SendSuper, which keeps Self bound to the child
// instance but looks the method up on Parent.
if sup, ok := e.Object.(*ast.SendExpr); ok {
if _, isSelf := sup.Object.(*ast.SelfExpr); isSelf &&
strings.EqualFold(sup.Method, "super") {
for _, arg := range e.Args {
g.emitExpr(arg)
}
// Emit defining-class name so runtime walks the right Parent
// chain — Self's class alone would infinite-loop on 3+ level
// hierarchies (Grand→Child→Base). See SendSuper comment.
g.writeln(fmt.Sprintf("t.SendSuper(%q, %q, %d)",
g.curMethodClass, e.Method, len(e.Args)))
return
}
}
// Self access: ::field (no parens) → PushSelfField
// Self method: ::method() (has parens) → Send on Self
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {

View File

@@ -48,14 +48,16 @@ func TestGenerateHelloWorld(t *testing.T) {
}
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.PopLocalFast(1)")
assertContains(t, code, "t.PushLocalFast(1)") // n
assertContains(t, code, "t.PushInt(5)")
assertContains(t, code, "t.Plus()")
assertContains(t, code, "t.RetValue()")
@@ -108,6 +110,8 @@ func TestGenerateForNext(t *testing.T) {
}
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
@@ -119,19 +123,22 @@ FUNCTION Main()
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.PushSymbol(t.VM().FindSymbol("DOUBLE"))`)
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.PushLocalFast(1)")
assertContains(t, code, "t.Plus()")
assertContains(t, code, `t.PushString("World")`)
assertContains(t, code, `t.PushString("!")`)
assertContains(t, code, "t.Plus()")
}
func TestGenerateSymbolTable(t *testing.T) {

View File

@@ -284,6 +284,53 @@ func (t *Thread) Send(methodName string, nArgs int) {
t.push(t.retVal)
}
// SendSuper dispatches a method call on Self, but starting the method
// lookup from the parent of the class that defined the currently-
// executing method. Implements `::super:Method(args)`.
//
// fromClassName is the class whose method body contains the ::super
// call — gengo emits it at compile time from the `METHOD ... CLASS X`
// declaration. Using Self's runtime class here would infinite-loop on
// 3-level hierarchies: Grand:New calls ::super:New → runs Child:New →
// Child:New calls ::super:New → would look up Grand.Parent = Child
// again, not Child.Parent = Base. Binding to the defining class is
// the same technique Harbour uses (method slot carries its origin
// class in the vtable).
//
// Stack: [arg1] ... [argN] → [result].
func (t *Thread) SendSuper(fromClassName, methodName string, nArgs int) {
args := make([]Value, nArgs)
for i := nArgs - 1; i >= 0; i-- {
args[i] = t.pop()
}
if !t.self.IsObject() {
panic(t.runtimeError("::super: outside method context"))
}
from := FindClass(fromClassName)
if from == nil {
panic(t.runtimeError(fmt.Sprintf("::super: unknown defining class %s", fromClassName)))
}
if from.Parent == nil {
panic(t.runtimeError(fmt.Sprintf("class %s has no parent for ::super", from.Name)))
}
parent := from.Parent
upper := strings.ToUpper(methodName)
fn, ok := parent.Methods[upper]
if !ok {
panic(t.runtimeError(fmt.Sprintf("unknown method %s in parent class %s", methodName, parent.Name)))
}
// Self unchanged — push args and invoke parent's slot.
for _, a := range args {
t.push(a)
}
t.pendingParams = nArgs
fn(t)
t.push(t.retVal)
}
// SendAssign dispatches a setter: obj:prop := value
// Generated for ::fieldName := value
func (t *Thread) SendAssign(fieldName string) {