Files
five/hbrt/ops_compare.go
CharlesKWON 66f045b97e 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>
2026-04-18 15:54:44 +09:00

483 lines
10 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Comparison and logical operations for the Five runtime.
// Implements Harbour-compatible comparison semantics including:
// - NIL == NIL → true, NIL == anything → false
// - Numeric cross-type comparison (Int/Long/Double auto-promotion)
// - String comparison respecting SET EXACT
// - Date/Timestamp comparison
// - Logical XOR equality (Clipper quirk)
//
// Operator hierarchy inspired by tsgo's isEqualityOperator/isRelationalOperatorOrHigher
// pattern (ref/typescript-go/internal/checker/utilities.go:772).
//
// See docs/harbour-type-system-analysis.md Section 6 for full rules.
package hbrt
import "strings"
// --- Equality operators ---
// 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]
t.stack[t.sp+1] = cachedNil
// Fast path: Int == Int
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) == int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
if valueEqual(a, b) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// ExactEqual pops two values, pushes boolean result.
// Harbour: hb_vmExactlyEqual — arrays cannot override.
func (t *Thread) ExactEqual() {
b := t.pop()
a := t.pop()
t.push(MakeBool(valueExactEqual(a, b)))
}
// 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]
t.stack[t.sp+1] = cachedNil
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) != int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
if !valueEqual(a, b) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// --- Relational operators ---
// 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]
t.stack[t.sp+1] = cachedNil
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) < int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
cmp, ok := valueCompare(a, b)
if !ok {
panic(t.argError("<", a, b))
}
if cmp < 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// 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]
t.stack[t.sp+1] = cachedNil
// Fast path: Int <= Int (most common in FOR loops)
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) <= int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
cmp, ok := valueCompare(a, b)
if !ok {
panic(t.argError("<=", a, b))
}
if cmp <= 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// 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]
t.stack[t.sp+1] = cachedNil
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) > int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
cmp, ok := valueCompare(a, b)
if !ok {
panic(t.argError(">", a, b))
}
if cmp > 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// 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]
t.stack[t.sp+1] = cachedNil
if a.Type() == tInt && b.Type() == tInt {
if int64(a.scalar) >= int64(b.scalar) {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
cmp, ok := valueCompare(a, b)
if !ok {
panic(t.argError(">=", a, b))
}
if cmp >= 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
}
// --- Logical operators ---
// Not negates the boolean value on top of stack.
func (t *Thread) Not() {
t.sp--
a := t.stack[t.sp]
// Fast path: logical not (most common — DO WHILE !EOF())
if a.Type() == tLogical {
if a.scalar != 0 {
t.stack[t.sp] = cachedFalse
} else {
t.stack[t.sp] = cachedTrue
}
t.sp++
return
}
panic(t.argError(".NOT.", a))
}
// And pops two values, pushes logical AND.
// Harbour evaluates both sides (no short-circuit in VM ops).
func (t *Thread) And() {
t.sp -= 2
a := t.stack[t.sp]
b := t.stack[t.sp+1]
t.stack[t.sp+1] = cachedNil
if a.Type() == tLogical && b.Type() == tLogical {
if a.scalar != 0 && b.scalar != 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
panic(t.argError(".AND.", a, b))
}
// Or pops two values, pushes logical OR.
func (t *Thread) Or() {
t.sp -= 2
a := t.stack[t.sp]
b := t.stack[t.sp+1]
t.stack[t.sp+1] = cachedNil
if a.Type() == tLogical && b.Type() == tLogical {
if a.scalar != 0 || b.scalar != 0 {
t.stack[t.sp] = cachedTrue
} else {
t.stack[t.sp] = cachedFalse
}
t.sp++
return
}
panic(t.argError(".OR.", a, b))
}
// InString implements the $ operator: "bc" $ "abcde" → .T.
func (t *Thread) InString() {
b := t.pop()
a := t.pop()
if a.IsString() && b.IsString() {
t.push(MakeBool(strings.Contains(b.AsString(), a.AsString())))
} else {
panic(t.argError("$", a, b))
}
}
// PopLogical pops the top of stack and returns it as bool.
// Used by generated code for IF/WHILE conditions.
// Harbour: hb_xvmPopLogical
func (t *Thread) PopLogical() bool {
t.sp--
v := t.stack[t.sp]
t.stack[t.sp] = cachedNil
// Fast path: check type tag directly (avoid method call overhead)
if v.Type() == tLogical {
return v.scalar != 0
}
// NIL → false (Harbour treats NIL as .F. in conditions)
if v.Type() == tNil {
return false
}
panic(t.argError("logical", v))
}
// --- Fused opcodes: combined stack operations for hot loops ---
// Eliminates 3-4 function calls per FOR iteration.
// LocalLessEqualInt: t.Local(idx) <= val (no stack ops). Byref-aware.
func (t *Thread) LocalLessEqualInt(localIdx, val int) bool {
v := t.locals[t.curFrame.localBase+localIdx-1]
if v.Type() == tByref {
v = (*HbRefCell)(v.ptr).V
}
if v.Type() == tInt {
return int64(v.scalar) <= int64(val)
}
if v.Type() == tDouble {
return v.AsNumDouble() <= float64(val)
}
return false
}
// LocalGreaterEqualInt: t.Local(idx) >= val (for descending FOR). Byref-aware.
func (t *Thread) LocalGreaterEqualInt(localIdx, val int) bool {
v := t.locals[t.curFrame.localBase+localIdx-1]
if v.Type() == tByref {
v = (*HbRefCell)(v.ptr).V
}
if v.Type() == tInt {
return int64(v.scalar) >= int64(val)
}
if v.Type() == tDouble {
return v.AsNumDouble() >= float64(val)
}
return false
}
// --- Optimized comparison (used by generated code) ---
// EqualIntIs compares stack top with an integer constant, returns bool.
// Harbour: hb_xvmEqualIntIs (fused PUSHINT + EQUAL)
func (t *Thread) EqualIntIs(n int64) bool {
a := t.pop()
if a.IsNumInt() {
return a.AsNumInt() == n
}
if a.IsDouble() {
return a.AsDouble() == float64(n)
}
return false
}
// --- Internal comparison functions ---
// valueEqual implements Harbour's equality semantics.
// NIL == NIL → true; NIL == anything → false
// Numeric: cross-type double comparison
// String: case-sensitive (SET EXACT ON assumed for now)
// Logical: XOR-like (Clipper quirk)
// Array/Hash/Block/Pointer: pointer identity
func valueEqual(a, b Value) bool {
at, bt := a.Type(), b.Type()
// NIL handling
if at == tNil && bt == tNil {
return true
}
if at == tNil || bt == tNil {
return false
}
// Numeric cross-type comparison
if a.IsNumeric() && b.IsNumeric() {
if a.IsNumInt() && b.IsNumInt() {
return a.AsNumInt() == b.AsNumInt()
}
return a.AsNumDouble() == b.AsNumDouble()
}
// Same type required from here
if at != bt {
return false
}
switch at {
case tString:
return a.AsString() == b.AsString()
case tDate:
return a.AsJulian() == b.AsJulian()
case tTimestamp:
return a.AsJulian() == b.AsJulian() && a.AsTimeMs() == b.AsTimeMs()
case tLogical:
// Harbour/Clipper quirk: XOR-like behavior
// .T. = .T. → .T. .F. = .F. → .T.
// .T. = .F. → .F. .F. = .T. → .F.
return a.AsBool() == b.AsBool()
case tArray, tObject:
// Pointer identity
return a.ptr == b.ptr
case tHash:
return a.ptr == b.ptr
case tBlock:
return a.ptr == b.ptr
case tPointer:
return a.scalar == b.scalar
}
return false
}
// valueExactEqual is like valueEqual but arrays/objects cannot override.
// Harbour: hb_vmExactlyEqual
func valueExactEqual(a, b Value) bool {
// For strings, exact equality checks full length (ignoring SET EXACT)
if a.IsString() && b.IsString() {
return a.AsString() == b.AsString()
}
return valueEqual(a, b)
}
// valueCompare returns comparison result (-1, 0, +1) and whether comparison is valid.
// Only String, Numeric, Date, Timestamp support ordering.
// Harbour: hb_vmLess, hb_vmGreater, etc. (hvm.c:4176+)
func valueCompare(a, b Value) (int, bool) {
// Numeric comparison
if a.IsNumeric() && b.IsNumeric() {
if a.IsNumInt() && b.IsNumInt() {
return compareInt64(a.AsNumInt(), b.AsNumInt()), true
}
return compareFloat64(a.AsNumDouble(), b.AsNumDouble()), true
}
at, bt := a.Type(), b.Type()
if at != bt {
return 0, false // type mismatch → error
}
switch at {
case tString:
return strings.Compare(a.AsString(), b.AsString()), true
case tDate:
return compareInt64(a.AsJulian(), b.AsJulian()), true
case tTimestamp:
cmp := compareInt64(a.AsJulian(), b.AsJulian())
if cmp != 0 {
return cmp, true
}
return compareInt32(a.AsTimeMs(), b.AsTimeMs()), true
}
return 0, false // unsupported type for ordering
}
// --- Primitive comparison helpers ---
// Following tsgo pattern of small, inlineable helper functions.
func compareInt64(a, b int64) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
func compareInt32(a, b int32) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
func compareFloat64(a, b float64) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}