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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user