diff --git a/compiler/gengo/gen_class.go b/compiler/gengo/gen_class.go index 661a569..f9570f8 100644 --- a/compiler/gengo/gen_class.go +++ b/compiler/gengo/gen_class.go @@ -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("") diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 93fb74b..4b7ddf6 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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 { diff --git a/compiler/gengo/gengo_test.go b/compiler/gengo/gengo_test.go index 8da88e1..e195039 100644 --- a/compiler/gengo/gengo_test.go +++ b/compiler/gengo/gengo_test.go @@ -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) { diff --git a/hbrt/class.go b/hbrt/class.go index 4d09499..a154073 100644 --- a/hbrt/class.go +++ b/hbrt/class.go @@ -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) {