Files
five/hbrt/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

451 lines
11 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// CLASS system for the Five runtime.
// Harbour: classes.c — object = array with uiClass, methods in class registry.
//
// Key concepts:
// - Class = definition (name, DATA fields, METHODs, parent)
// - Object = HbArray with Class > 0 (fields stored in Items[])
// - Send = method dispatch: obj:method(args) → lookup class → call func
// - :: = Self access (current object in method context)
// - INHERIT FROM = parent class embedding
// - Operator overloading: HB_OO_OP_PLUS etc.
//
// Reference:
// /mnt/d/harbour-core/include/hbapicls.h
// /mnt/d/harbour-core/src/vm/classes.c
package hbrt
import (
"fmt"
"strings"
"sync"
)
// ClassDef defines a class (DATA fields + METHODs).
// Harbour: internal class structure in classes.c
type ClassDef struct {
ID uint16
Name string
Parent *ClassDef // INHERIT FROM
Fields []ClassField // DATA declarations (ordered)
Methods map[string]MethodFunc // method name → function
Operators [MaxOperator + 1]MethodFunc // operator overloading
fieldMap map[string]int // field name → index
}
// ClassField describes a DATA member.
type ClassField struct {
Name string
Init Value // default INIT value
AsType string // optional type hint
}
// MethodFunc is the signature for class methods.
// The method receives the thread; Self is available via thread context.
type MethodFunc func(t *Thread)
// Operator IDs matching Harbour's HB_OO_OP_*
const (
OpPlus = 0
OpMinus = 1
OpMult = 2
OpDivide = 3
OpMod = 4
OpPower = 5
OpInc = 6
OpDec = 7
OpEqual = 8
OpExactEqual = 9
OpNotEqual = 10
OpLess = 11
OpLessEqual = 12
OpGreater = 13
OpGreaterEqual = 14
OpAssign = 15
OpInString = 16
OpInclude = 17
OpNot = 18
OpAnd = 19
OpOr = 20
OpArrayIndex = 21
MaxOperator = 21
)
// --- Class Registry ---
var (
classRegMu sync.Mutex // full mutex (not RW) — prevents slice reallocation race on classList
classReg = map[string]*ClassDef{}
classList []*ClassDef // index = classID - 1
)
// RegisterClass registers a class definition.
// Returns the assigned class ID (1-based).
func RegisterClass(cls *ClassDef) uint16 {
classRegMu.Lock()
defer classRegMu.Unlock()
cls.ID = uint16(len(classList) + 1)
classList = append(classList, cls)
classReg[strings.ToUpper(cls.Name)] = cls
// Build field index map
cls.fieldMap = make(map[string]int, len(cls.Fields))
for i, f := range cls.Fields {
cls.fieldMap[strings.ToUpper(f.Name)] = i
}
return cls.ID
}
// ListClassNames returns all registered class names, sorted by registration
// order (1-based class IDs). Used by the diagnostic ErrorLog writer.
func ListClassNames() []string {
classRegMu.Lock()
defer classRegMu.Unlock()
out := make([]string, 0, len(classList))
for _, c := range classList {
if c != nil {
out = append(out, c.Name)
}
}
return out
}
// FindClass looks up a class by name.
func FindClass(name string) *ClassDef {
classRegMu.Lock()
defer classRegMu.Unlock()
return classReg[strings.ToUpper(name)]
}
// GetClass looks up a class by ID.
func GetClass(id uint16) *ClassDef {
classRegMu.Lock()
defer classRegMu.Unlock()
if int(id) > 0 && int(id) <= len(classList) {
return classList[id-1]
}
return nil
}
// --- Class builder (fluent API for generated code) ---
// NewClassDef creates a new class definition builder.
func NewClassDef(name string) *ClassDef {
return &ClassDef{
Name: name,
Methods: make(map[string]MethodFunc),
}
}
// InheritFrom sets the parent class.
func (c *ClassDef) InheritFrom(parentName string) *ClassDef {
parent := FindClass(parentName)
if parent != nil {
c.Parent = parent
// Copy parent fields first
c.Fields = append(append([]ClassField{}, parent.Fields...), c.Fields...)
// Copy parent methods (child can override)
for name, fn := range parent.Methods {
if _, exists := c.Methods[name]; !exists {
c.Methods[name] = fn
}
}
// Copy parent operators
for i, fn := range parent.Operators {
if c.Operators[i] == nil {
c.Operators[i] = fn
}
}
}
return c
}
// AddData adds a DATA field to the class.
func (c *ClassDef) AddData(name string, init Value) *ClassDef {
c.Fields = append(c.Fields, ClassField{Name: name, Init: init})
return c
}
// AddMethod adds a METHOD to the class.
func (c *ClassDef) AddMethod(name string, fn MethodFunc) *ClassDef {
c.Methods[strings.ToUpper(name)] = fn
return c
}
// AddOperator sets an operator overload.
func (c *ClassDef) AddOperator(op int, fn MethodFunc) *ClassDef {
if op >= 0 && op <= MaxOperator {
c.Operators[op] = fn
}
return c
}
// Register registers this class and returns the class ID.
func (c *ClassDef) Register() uint16 {
return RegisterClass(c)
}
// FieldIndex returns the 0-based field index by name, or -1.
func (c *ClassDef) FieldIndex(name string) int {
if c.fieldMap == nil {
return -1
}
if idx, ok := c.fieldMap[strings.ToUpper(name)]; ok {
return idx
}
// Check parent
if c.Parent != nil {
return c.Parent.FieldIndex(name)
}
return -1
}
// --- Object creation ---
// NewObject creates a new object instance of a class.
// Harbour: object = array with uiClass set.
func NewObject(classID uint16) Value {
cls := GetClass(classID)
if cls == nil {
return MakeNil()
}
obj := MakeObject(classID, len(cls.Fields))
arr := obj.AsArray()
// Initialize fields with default values
for i, f := range cls.Fields {
arr.Items[i] = f.Init
}
return obj
}
// --- Method dispatch ---
// Send dispatches a method call on an object.
// Harbour: hb_objGetMethod + call
// Stack: [object] [arg1] ... [argN] → call method → [result]
func (t *Thread) Send(methodName string, nArgs int) {
// Collect args
args := make([]Value, nArgs)
for i := nArgs - 1; i >= 0; i-- {
args[i] = t.pop()
}
objVal := t.pop() // object
if !objVal.IsObject() {
// Not a class object — try built-in Value methods (String:Upper, Array:Sort, etc.)
if result, ok := SendBuiltin(t, objVal, methodName, args); ok {
t.push(result)
return
}
panic(t.runtimeError(fmt.Sprintf("not an object for method %s", methodName)))
}
arr := objVal.AsArray()
cls := GetClass(arr.Class)
if cls == nil {
panic(t.runtimeError(fmt.Sprintf("unknown class ID %d", arr.Class)))
}
upperMethod := strings.ToUpper(methodName)
// Check for data field access (getter)
if nArgs == 0 {
if idx := cls.FieldIndex(methodName); idx >= 0 {
t.push(arr.Items[idx])
return
}
}
// Check for data field setter: _FIELDNAME convention
if nArgs == 1 && strings.HasPrefix(upperMethod, "_") {
fieldName := upperMethod[1:]
if idx := cls.FieldIndex(fieldName); idx >= 0 {
arr.Items[idx] = args[0]
t.push(args[0])
return
}
}
// Look up method
fn, ok := cls.Methods[upperMethod]
if !ok {
panic(t.runtimeError(fmt.Sprintf("unknown method %s in class %s", methodName, cls.Name)))
}
// Set up Self context
oldSelf := t.self
t.self = objVal
// Push args for Frame
for _, arg := range args {
t.push(arg)
}
t.pendingParams = nArgs
fn(t)
// Restore Self
t.self = oldSelf
// Push return value
t.push(t.retVal)
}
// tryBinaryOp checks whether the LHS of a pending binary operation is
// an object whose class overloads the given operator slot. If so, it
// dispatches the overload (Self=LHS, one positional arg = RHS) and
// returns true with the result pushed in place of the two operands.
// Returns false for non-object LHS or classes without an overload,
// letting the caller fall through to the built-in op.
func (t *Thread) tryBinaryOp(op int) bool {
if t.sp < 2 {
return false
}
a := t.stack[t.sp-2]
if !a.IsObject() {
return false
}
// AsArray can return nil if the ptr field is unset despite an object
// tag — a corrupted Value that would otherwise crash at `.Class`.
// Guard defensively; correct construction paths never hit this.
arr := a.AsArray()
if arr == nil {
return false
}
cls := GetClass(arr.Class)
if cls == nil || cls.Operators[op] == nil {
return false
}
fn := cls.Operators[op]
// Stack layout: [a] [b] → caller expects [result] after return.
b := t.pop()
t.pop() // discard a (Self takes over)
oldSelf := t.self
t.self = a
t.push(b)
t.pendingParams = 1
fn(t)
t.self = oldSelf
t.push(t.retVal)
return true
}
// SendSuper dispatches a method call on Self, but starting the method
// lookup from the parent of the class that defined the currently-
// executing method. Implements `::super:Method(args)`.
//
// fromClassName is the class whose method body contains the ::super
// call — gengo emits it at compile time from the `METHOD ... CLASS X`
// declaration. Using Self's runtime class here would infinite-loop on
// 3-level hierarchies: Grand:New calls ::super:New → runs Child:New →
// Child:New calls ::super:New → would look up Grand.Parent = Child
// again, not Child.Parent = Base. Binding to the defining class is
// the same technique Harbour uses (method slot carries its origin
// class in the vtable).
//
// Stack: [arg1] ... [argN] → [result].
func (t *Thread) SendSuper(fromClassName, methodName string, nArgs int) {
args := make([]Value, nArgs)
for i := nArgs - 1; i >= 0; i-- {
args[i] = t.pop()
}
if !t.self.IsObject() {
panic(t.runtimeError("::super: outside method context"))
}
from := FindClass(fromClassName)
if from == nil {
panic(t.runtimeError(fmt.Sprintf("::super: unknown defining class %s", fromClassName)))
}
if from.Parent == nil {
panic(t.runtimeError(fmt.Sprintf("class %s has no parent for ::super", from.Name)))
}
parent := from.Parent
upper := strings.ToUpper(methodName)
fn, ok := parent.Methods[upper]
if !ok {
panic(t.runtimeError(fmt.Sprintf("unknown method %s in parent class %s", methodName, parent.Name)))
}
// Self unchanged — push args and invoke parent's slot.
for _, a := range args {
t.push(a)
}
t.pendingParams = nArgs
fn(t)
t.push(t.retVal)
}
// SendAssign dispatches a setter: obj:prop := value
// Generated for ::fieldName := value
func (t *Thread) SendAssign(fieldName string) {
val := t.pop()
objVal := t.pop()
if !objVal.IsObject() {
panic(t.runtimeError("not an object for assignment"))
}
arr := objVal.AsArray()
cls := GetClass(arr.Class)
if cls == nil {
return
}
if idx := cls.FieldIndex(fieldName); idx >= 0 {
arr.Items[idx] = val
}
}
// Send0 dispatches a no-arg method (getter).
func (t *Thread) Send0(methodName string) {
t.Send(methodName, 0)
}
// PushSelfField pushes a field from the current Self object.
// Used by :: access in methods.
func (t *Thread) PushSelfField(fieldName string) {
if t.self.IsNil() {
t.push(MakeNil())
return
}
arr := t.self.AsArray()
cls := GetClass(arr.Class)
if cls != nil {
if idx := cls.FieldIndex(fieldName); idx >= 0 {
t.push(arr.Items[idx])
return
}
}
t.push(MakeNil())
}
// SetSelfField sets a field on the current Self object.
func (t *Thread) SetSelfField(fieldName string) {
val := t.pop()
if t.self.IsNil() {
return
}
arr := t.self.AsArray()
cls := GetClass(arr.Class)
if cls != nil {
if idx := cls.FieldIndex(fieldName); idx >= 0 {
arr.Items[idx] = val
}
}
}
// GetSelf returns the current Self value.
func (t *Thread) GetSelf() Value {
return t.self
}