feat(oop): OPERATOR overloading — + - * / == != < > <= >=

Harbour lets a class define custom behaviour for arithmetic and
comparison operators via `OPERATOR "<sym>" ARG <name> INLINE <expr>`.
Five already had the runtime slot infrastructure (ClassDef.Operators
+ AddOperator + parent-chain copy) but parser skipped the form and
the VM ops never consulted the slots.

Parser: parseOperatorDecl captures the symbol, ARG binding, and
INLINE body into a MethodDecl with IsOperator=true and OperatorOp
set to the hbrt.Op* slot. Synthesised method name is __OP_<idx>
to keep the regular method namespace clean.

Codegen: emitClassDecl routes IsOperator members through
_def.AddOperator instead of AddMethod. Inline body generation is
shared with the MESSAGE/INLINE path (34485cd).

VM: Thread.tryBinaryOp walks the LHS object's class operator slot,
pushes args with Self bound to LHS, and returns true if the slot
is populated. Wired into Plus/Minus/Mult/Divide and Equal/NotEqual/
Less/Greater/LessEqual/GreaterEqual. Falls through to built-in
behaviour when no overload exists — non-object LHS costs one tag
check per op.

Operator symbol→slot mapping keeps `=` and `==` on the same slot
(OpEqual=8) because Five's gengo routes both to t.Equal() and the
VM doesn't distinguish strict vs non-strict equality today.

Tested (/tmp/test_operator.prg): Vec2 + - == < with per-field
results all correct.

FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:54:44 +09:00
parent 34485cd6c8
commit 66f045b97e
6 changed files with 159 additions and 7 deletions

View File

@@ -178,6 +178,8 @@ type MethodDecl struct {
Params []*ParamDecl
IsInline bool // INLINE method
InlineBody Expr // inline expression body — `RETURN <expr>` 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

View File

@@ -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))
}
}

View File

@@ -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 "<sym>" ARG <name> INLINE <expr>
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_<CLASS>_<NAME> function.
members = append(members, p.parseMessageDecl())
} else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" ||
} else if upper == "OPERATOR" {
// OPERATOR "<sym>" ARG <name> INLINE <expr>
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 "<sym>" [ARG <name>] INLINE <expr>
//
// The ARG binds the RHS operand to a local when the operator is
// dispatched via the VM's binary op. INLINE <expr> is the body.
// Harbour also allows `OPERATOR "<sym>" ... 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_<idx> 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 <name> [(params)] INLINE <expr>` in
// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE
// handler is invoked like a method and behaves identically — the form