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>
451 lines
11 KiB
Go
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
|
|
}
|