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>
483 lines
10 KiB
Go
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
|
|
}
|