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

@@ -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) {