Files
five/compiler/gengo/gen_class.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- go test ./...        : PASS
- FiveSql2 SQL:1999    : 43/43
- Harbour compat       : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:26:25 +09:00

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