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