Files
five/compiler/gengo/gen_class.go
CharlesKWON 34485cd6c8 feat(oop): METHOD ... INLINE <expr> and MESSAGE handlers
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>
2026-04-18 15:41:36 +09:00

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()"
}