// 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("FV_%s_%s", className, upperName) switch { case md.IsOperator: // OPERATOR slot — don't pollute the method table. g.writeln(fmt.Sprintf("_def.AddOperator(%d, %s)", md.OperatorOp, goFuncName)) case 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)) case md.IsAccess: // ACCESS propName METHOD getterName g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", strings.ToUpper(md.AccessName), goFuncName)) case md.IsAssign: // ASSIGN propName METHOD setterName g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", "_"+strings.ToUpper(md.AccessName), goFuncName)) default: 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 FV_%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 FV__ 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("FV_%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("FV_%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) } } // Mid-method and nested LOCAL declarations (inside IF / FOR / // WHILE / DO CASE / SEQUENCE / WATCH / TIMEOUT / PARALLEL FOR) // must also be counted into the runtime frame size. The // FuncDecl emitter already walks the body via // countLocalsInStmts; methods used to short-circuit this and // only count top-level Decls, so `METHOD Foo(): … IF cond … // LOCAL x …` underallocated the frame and `x` either read NIL // or stomped a sibling local. nLocals += countLocalsInStmts(md.Body) 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++ } } } // Pre-allocate slots for body-nested LOCALs so emitStmt's // mid-function VarDecl handler stores into the right index. scanBodyLocals(md.Body, localMap, &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()" }