Harbour's inline-method sugar was parsed but the body was skipped, leaving any `METHOD X() INLINE expr` declaration registered in the class vtable with no matching HB_<CLASS>_X function — link error at build time. Parser: MethodDecl gains an InlineBody Expr field. parseClassMethodDecl captures the expression after INLINE instead of skipping to EOL. New parseMessageDecl handles `MESSAGE <name> [(params)] INLINE expr` and returns the same MethodDecl shape. Codegen: emitClassDecl walks members a second time after the class registration init block and emits emitInlineMethodBody for each IsInline method — a Frame(nParams, 0) + emitExpr(InlineBody) + RetValue function. curMethodClass is bound so ::super: inside an inline body still resolves. Tested (/tmp/test_inline.prg): all four patterns — bare INLINE, MESSAGE INLINE, INLINE with params, INLINE reading ::field — produce expected values. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
231 lines
6.4 KiB
Go
231 lines
6.4 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// CLASS code generation for Five.
|
|
// Generates Go code that registers classes with hbrt.ClassDef.
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"five/compiler/token"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// emitClassDecl generates class registration code.
|
|
// CLASS Person
|
|
// DATA cName INIT ""
|
|
// DATA nAge INIT 0
|
|
// METHOD New(cName, nAge)
|
|
// ENDCLASS
|
|
// →
|
|
// func init() { hbrt.NewClassDef("Person").AddData(...).Register() }
|
|
func (g *Generator) emitClassDecl(cls *ast.ClassDecl) {
|
|
className := strings.ToUpper(cls.Name)
|
|
varName := "_cls_" + className
|
|
|
|
g.writeln(fmt.Sprintf("var %s uint16", varName))
|
|
g.writeln("")
|
|
g.writeln("func init() {")
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("_def := hbrt.NewClassDef(%q)", cls.Name))
|
|
|
|
// Parent
|
|
if cls.ParentName != "" {
|
|
g.writeln(fmt.Sprintf("_def.InheritFrom(%q)", cls.ParentName))
|
|
}
|
|
|
|
// DATA fields
|
|
for _, m := range cls.Members {
|
|
if dd, ok := m.(*ast.DataDecl); ok {
|
|
initVal := "hbrt.MakeNil()"
|
|
if dd.Init != nil {
|
|
initVal = g.exprToGoLiteral(dd.Init)
|
|
}
|
|
g.writeln(fmt.Sprintf("_def.AddData(%q, %s)", strings.ToUpper(dd.Name), initVal))
|
|
}
|
|
}
|
|
|
|
// METHOD declarations (link to Go functions)
|
|
for _, m := range cls.Members {
|
|
if md, ok := m.(*ast.MethodDecl); ok {
|
|
upperName := strings.ToUpper(md.Name)
|
|
goFuncName := fmt.Sprintf("HB_%s_%s", className, upperName)
|
|
|
|
if md.IsSetGet {
|
|
// SETGET: register as both getter and setter
|
|
// Getter = method name, Setter = _name
|
|
g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", upperName, goFuncName))
|
|
g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", "_"+upperName, goFuncName))
|
|
} else if md.IsAccess {
|
|
// ACCESS propName METHOD getterName
|
|
g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", strings.ToUpper(md.AccessName), goFuncName))
|
|
} else if md.IsAssign {
|
|
// ASSIGN propName METHOD setterName
|
|
g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", "_"+strings.ToUpper(md.AccessName), goFuncName))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", upperName, goFuncName))
|
|
}
|
|
}
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("%s = _def.Register()", varName))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
|
|
// Emit function bodies for INLINE methods — the class-body form
|
|
// `METHOD X() INLINE expr` / `MESSAGE X INLINE expr` carries its
|
|
// own body, unlike plain METHOD declarations which expect a
|
|
// standalone `METHOD X() CLASS Foo` implementation elsewhere.
|
|
for _, m := range cls.Members {
|
|
md, ok := m.(*ast.MethodDecl)
|
|
if !ok || !md.IsInline || md.InlineBody == nil {
|
|
continue
|
|
}
|
|
g.emitInlineMethodBody(className, md)
|
|
}
|
|
|
|
// Also need a constructor function: Person() returns new object
|
|
// This is called as Person():New(...)
|
|
g.writeln(fmt.Sprintf("func HB_%s_CTOR(t *hbrt.Thread) {", className))
|
|
g.indent++
|
|
g.writeln("t.Frame(0, 0)")
|
|
g.writeln("defer t.EndProc()")
|
|
g.writeln(fmt.Sprintf("t.PushValue(hbrt.NewObject(%s))", varName))
|
|
g.writeln("t.RetValue()")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
|
|
// Constructor symbol already added in Generate() symbol collection phase
|
|
}
|
|
|
|
// emitInlineMethodBody generates the HB_<CLASS>_<METHOD> function for
|
|
// an INLINE-declared method: the body is the single expression parsed
|
|
// after the INLINE keyword, evaluated and returned. Params bind to
|
|
// locals 1..N so the inline expression can reference them.
|
|
func (g *Generator) emitInlineMethodBody(className string, md *ast.MethodDecl) {
|
|
methodName := strings.ToUpper(md.Name)
|
|
goFuncName := fmt.Sprintf("HB_%s_%s", className, methodName)
|
|
nParams := len(md.Params)
|
|
|
|
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goFuncName))
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, 0)", nParams))
|
|
g.writeln("defer t.EndProc()")
|
|
|
|
// Param name → local index map so the inline expr can reference them.
|
|
localMap := make(localMap)
|
|
for i, p := range md.Params {
|
|
localMap[strings.ToUpper(p.Name)] = i + 1
|
|
}
|
|
prevLocals := g.curLocals
|
|
prevCls := g.curMethodClass
|
|
g.curLocals = localMap
|
|
g.curMethodClass = className
|
|
|
|
g.emitExpr(md.InlineBody)
|
|
g.writeln("t.RetValue()")
|
|
|
|
g.curLocals = prevLocals
|
|
g.curMethodClass = prevCls
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
}
|
|
|
|
// emitMethodDeclStandalone generates a standalone METHOD ... CLASS ... implementation.
|
|
func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
|
|
if md.ClassName == "" {
|
|
return // in-class method declaration only (no body)
|
|
}
|
|
|
|
className := strings.ToUpper(md.ClassName)
|
|
methodName := strings.ToUpper(md.Name)
|
|
goFuncName := fmt.Sprintf("HB_%s_%s", className, methodName)
|
|
|
|
nParams := len(md.Params)
|
|
nLocals := 0
|
|
for _, d := range md.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok {
|
|
nLocals += len(vd.Vars)
|
|
}
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goFuncName))
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
g.writeln("")
|
|
|
|
// Build local map
|
|
localMap := make(localMap)
|
|
idx := 1
|
|
for _, p := range md.Params {
|
|
localMap[strings.ToUpper(p.Name)] = idx
|
|
idx++
|
|
}
|
|
for _, d := range md.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok {
|
|
for _, v := range vd.Vars {
|
|
if v.Init != nil {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
localMap[strings.ToUpper(v.Name)] = idx
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
|
|
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("")
|
|
}
|
|
|
|
// exprToGoLiteral converts a simple AST expression to a Go literal string.
|
|
// Used for DATA INIT values.
|
|
func (g *Generator) exprToGoLiteral(expr ast.Expr) string {
|
|
switch e := expr.(type) {
|
|
case *ast.LiteralExpr:
|
|
switch e.Kind {
|
|
case token.INT:
|
|
return fmt.Sprintf("hbrt.MakeInt(%s)", e.Value)
|
|
case token.DOUBLE:
|
|
return fmt.Sprintf("hbrt.MakeDoubleAuto(%s)", e.Value)
|
|
case token.STRING:
|
|
return fmt.Sprintf("hbrt.MakeString(%q)", e.Value)
|
|
case token.TRUE:
|
|
return "hbrt.MakeBool(true)"
|
|
case token.FALSE:
|
|
return "hbrt.MakeBool(false)"
|
|
case token.NIL_LIT:
|
|
return "hbrt.MakeNil()"
|
|
}
|
|
case *ast.ArrayLitExpr:
|
|
// {} empty array or {1,2,3}
|
|
if len(e.Items) == 0 {
|
|
return "hbrt.MakeArray(0)"
|
|
}
|
|
// Non-empty arrays need runtime construction — fall through to nil
|
|
case *ast.HashLitExpr:
|
|
if len(e.Keys) == 0 {
|
|
return "hbrt.MakeHash()"
|
|
}
|
|
}
|
|
return "hbrt.MakeNil()"
|
|
}
|