// 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. Restore via defer so a panic in the // method body (HbError, BreakValue from `Break(…)`, runtime // panic) unwinds with `t.self` pointing at the caller's // receiver — not at this method's. Without defer, a RECOVER // USING handler that runs after the panic still saw the stale // `t.self`, so `::field` / `::method()` resolved against the // wrong object — silent data corruption in the recovery path. oldSelf := t.self t.self = objVal defer func() { t.self = oldSelf }() // Push args for Frame for _, arg := range args { t.push(arg) } t.pendingParams = nArgs fn(t) // 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 // defer restore — see comment in Send. defer func() { t.self = oldSelf }() t.push(b) t.pendingParams = 1 fn(t) 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 }