// 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 }