diff --git a/compiler/ast/ast.go b/compiler/ast/ast.go index 99ae418..38d0493 100644 --- a/compiler/ast/ast.go +++ b/compiler/ast/ast.go @@ -178,6 +178,8 @@ type MethodDecl struct { Params []*ParamDecl IsInline bool // INLINE method InlineBody Expr // inline expression body — `RETURN ` equivalent + IsOperator bool // OPERATOR overload — OperatorOp carries the slot + OperatorOp int // operator slot (hbrt.Op* constant); valid only when IsOperator IsSetGet bool // METHOD name(x) SETGET — getter if no arg, setter if arg IsAccess bool // ACCESS name METHOD getterName IsAssign bool // ASSIGN name METHOD setterName diff --git a/compiler/gengo/gen_class.go b/compiler/gengo/gen_class.go index e6598e1..4226f10 100644 --- a/compiler/gengo/gen_class.go +++ b/compiler/gengo/gen_class.go @@ -52,18 +52,22 @@ func (g *Generator) emitClassDecl(cls *ast.ClassDecl) { upperName := strings.ToUpper(md.Name) goFuncName := fmt.Sprintf("HB_%s_%s", className, upperName) - if md.IsSetGet { + 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)) - } else if md.IsAccess { + case md.IsAccess: // ACCESS propName METHOD getterName g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", strings.ToUpper(md.AccessName), goFuncName)) - } else if md.IsAssign { + case md.IsAssign: // ASSIGN propName METHOD setterName g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", "_"+strings.ToUpper(md.AccessName), goFuncName)) - } else { + default: g.writeln(fmt.Sprintf("_def.AddMethod(%q, %s)", upperName, goFuncName)) } } diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 8ac8d29..b35f205 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -564,8 +564,13 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { } else { p.skipToEndOfLine() } - case token.INLINE_KW, token.ON, token.DESTRUCTOR, token.OPERATOR_KW: - // Stray INLINE, ON ERROR, DESTRUCTOR, OPERATOR — skip to EOL + case token.OPERATOR_KW: + // OPERATOR "" ARG INLINE + if md := p.parseOperatorDecl(); md != nil { + members = append(members, md) + } + case token.INLINE_KW, token.ON, token.DESTRUCTOR: + // Stray INLINE, ON ERROR, DESTRUCTOR — skip to EOL p.skipToEndOfLine() p.skipNewlines() continue @@ -616,7 +621,12 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { // emitClassDecl routes it through AddMethod and the inline // body emitter generates the HB__ function. members = append(members, p.parseMessageDecl()) - } else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" || + } else if upper == "OPERATOR" { + // OPERATOR "" ARG INLINE + if md := p.parseOperatorDecl(); md != nil { + members = append(members, md) + } + } else if upper == "ON" || upper == "DESTRUCTOR" || upper == "DELEGATE" || upper == "ERROR" || upper == "VIRTUAL" || upper == "DEFERRED" { // ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL @@ -771,6 +781,79 @@ func (p *Parser) parseClassMethodDecl() *ast.MethodDecl { } } +// operatorNameIndex maps a Harbour operator symbol to its slot in +// hbrt.ClassDef.Operators. Values mirror hbrt.Op* constants +// (hbrt/class.go:50-73). +// +// `=` and `==` both route through VM Thread.Equal(), which dispatches +// on OpEqual (8) — Five doesn't distinguish them at the VM level, so +// `==` maps to the same slot. `!=`, `<>`, `#` are all OpNotEqual (10). +var operatorNameIndex = map[string]int{ + "+": 0, "-": 1, "*": 2, "/": 3, "%": 4, "^": 5, + "++": 6, "--": 7, + "=": 8, "==": 8, "!=": 10, "<>": 10, "#": 10, + "<": 11, "<=": 12, ">": 13, ">=": 14, + ":=": 15, "$": 16, +} + +// parseOperatorDecl parses: +// OPERATOR "" [ARG ] INLINE +// +// The ARG binds the RHS operand to a local when the operator is +// dispatched via the VM's binary op. INLINE is the body. +// Harbour also allows `OPERATOR "" ... METHOD foo CLASS X` +// (non-inline) but that form is rare; we only support INLINE for +// now and skip the line otherwise. +func (p *Parser) parseOperatorDecl() *ast.MethodDecl { + opPos := p.current.Pos + p.advance() // OPERATOR + + if p.current.Kind != token.STRING { + p.skipToEndOfLine() + return nil + } + symbol := p.current.Literal + p.advance() + + opIdx, ok := operatorNameIndex[symbol] + if !ok { + p.skipToEndOfLine() + return nil + } + + var params []*ast.ParamDecl + if p.current.Kind == token.IDENT && p.currentUpper() == "ARG" { + p.advance() + argName := p.expectMethodName().Literal + params = []*ast.ParamDecl{{Name: argName}} + } + + if !(p.current.Kind == token.INLINE_KW || + (p.current.Kind == token.IDENT && p.currentUpper() == "INLINE")) { + // Non-inline operator — body arrives as a separate METHOD decl. + // Unsupported for now; skip. + p.skipToEndOfLine() + return nil + } + p.advance() // INLINE + body := p.parseExpr() + p.expectEndOfStmt() + + // Synthesise a method named __OP_ so the vtable doesn't collide + // with user-declared methods. emitClassDecl sees OperatorOp >= 0 and + // routes registration through AddOperator instead of AddMethod. + return &ast.MethodDecl{ + MethodPos: opPos, + Name: fmt.Sprintf("__OP_%d", opIdx), + Params: params, + IsInline: true, + InlineBody: body, + IsOperator: true, + OperatorOp: opIdx, + EndPos: opPos, + } +} + // parseMessageDecl parses `MESSAGE [(params)] INLINE ` in // a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE // handler is invoked like a method and behaves identically — the form diff --git a/hbrt/class.go b/hbrt/class.go index a154073..e876547 100644 --- a/hbrt/class.go +++ b/hbrt/class.go @@ -284,6 +284,39 @@ func (t *Thread) Send(methodName string, nArgs int) { 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 + } + cls := GetClass(a.AsArray().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)`. diff --git a/hbrt/ops_arith.go b/hbrt/ops_arith.go index 2a8f179..82d03ef 100644 --- a/hbrt/ops_arith.go +++ b/hbrt/ops_arith.go @@ -20,6 +20,9 @@ import "math" // Date + Numeric -> Date // Timestamp + Numeric -> Timestamp func (t *Thread) Plus() { + if t.tryBinaryOp(OpPlus) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -88,6 +91,9 @@ func (t *Thread) Plus() { // Minus pops two values, pushes their difference. // Harbour: hb_vmMinus (hvm.c:3401) func (t *Thread) Minus() { + if t.tryBinaryOp(OpMinus) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -145,6 +151,9 @@ func (t *Thread) Minus() { // Harbour: hb_vmMult (hvm.c:3510) // Decimal rule: dec = dec1 + dec2 func (t *Thread) Mult() { + if t.tryBinaryOp(OpMult) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -186,6 +195,9 @@ func (t *Thread) Mult() { // Harbour: hb_vmDivide (hvm.c:3546) // Always returns Double. Division by zero -> runtime error. func (t *Thread) Divide() { + if t.tryBinaryOp(OpDivide) { + return + } b := t.pop() a := t.pop() diff --git a/hbrt/ops_compare.go b/hbrt/ops_compare.go index 4d0c274..e000347 100644 --- a/hbrt/ops_compare.go +++ b/hbrt/ops_compare.go @@ -22,6 +22,9 @@ import "strings" // Equal pops two values, pushes boolean result. // Harbour: hb_vmEqual (hvm.c:3974) func (t *Thread) Equal() { + if t.tryBinaryOp(OpEqual) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -54,6 +57,9 @@ func (t *Thread) ExactEqual() { // NotEqual pops two values, pushes boolean result. func (t *Thread) NotEqual() { + if t.tryBinaryOp(OpNotEqual) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -80,6 +86,9 @@ func (t *Thread) NotEqual() { // Less pops two values, pushes boolean result. // Harbour: hb_vmLess (hvm.c:4176) func (t *Thread) Less() { + if t.tryBinaryOp(OpLess) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -107,6 +116,9 @@ func (t *Thread) Less() { // LessEqual pops two values, pushes boolean result. func (t *Thread) LessEqual() { + if t.tryBinaryOp(OpLessEqual) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -135,6 +147,9 @@ func (t *Thread) LessEqual() { // Greater pops two values, pushes boolean result. func (t *Thread) Greater() { + if t.tryBinaryOp(OpGreater) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1] @@ -162,6 +177,9 @@ func (t *Thread) Greater() { // GreaterEqual pops two values, pushes boolean result. func (t *Thread) GreaterEqual() { + if t.tryBinaryOp(OpGreaterEqual) { + return + } t.sp -= 2 a := t.stack[t.sp] b := t.stack[t.sp+1]