Harbour lets a class define custom behaviour for arithmetic and
comparison operators via `OPERATOR "<sym>" ARG <name> INLINE <expr>`.
Five already had the runtime slot infrastructure (ClassDef.Operators
+ AddOperator + parent-chain copy) but parser skipped the form and
the VM ops never consulted the slots.
Parser: parseOperatorDecl captures the symbol, ARG binding, and
INLINE body into a MethodDecl with IsOperator=true and OperatorOp
set to the hbrt.Op* slot. Synthesised method name is __OP_<idx>
to keep the regular method namespace clean.
Codegen: emitClassDecl routes IsOperator members through
_def.AddOperator instead of AddMethod. Inline body generation is
shared with the MESSAGE/INLINE path (34485cd).
VM: Thread.tryBinaryOp walks the LHS object's class operator slot,
pushes args with Self bound to LHS, and returns true if the slot
is populated. Wired into Plus/Minus/Mult/Divide and Equal/NotEqual/
Less/Greater/LessEqual/GreaterEqual. Falls through to built-in
behaviour when no overload exists — non-object LHS costs one tag
check per op.
Operator symbol→slot mapping keeps `=` and `==` on the same slot
(OpEqual=8) because Five's gengo routes both to t.Equal() and the
VM doesn't distinguish strict vs non-strict equality today.
Tested (/tmp/test_operator.prg): Vec2 + - == < with per-field
results all correct.
FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
6.6 KiB
Go
235 lines
6.6 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)
|
|
|
|
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 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()"
|
|
}
|