value.go: - cachedNil, cachedTrue, cachedFalse: pre-built constant Values - MakeBool()/MakeNil(): return cached (zero allocation) - smallInts[256]: pre-built integers 0-255 (skip intExpLen loop) - MakeInt(): fast path for 0-255 thread.go: - pop(): use cachedNil for GC help (no MakeNil() call) ops_compare.go: - LessEqual(): inline Int-Int fast path (skip valueCompare) Direct scalar comparison with cached bool result - Not(): inline logical fast path (skip IsLogical+AsBool) - PopLogical(): inline type check + scalar read Impact: these functions called millions of times in FOR/DO WHILE loops. 10K SEEK: 20ms → 16ms (20%). CDX SCOPE: 12ms → 9ms (25%). 82/82 stress PASS. 14 packages ALL PASS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
332 lines
7.4 KiB
Go
332 lines
7.4 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() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
t.push(MakeBool(valueEqual(a, b)))
|
|
}
|
|
|
|
// 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() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
t.push(MakeBool(!valueEqual(a, b)))
|
|
}
|
|
|
|
// --- Relational operators ---
|
|
|
|
// Less pops two values, pushes boolean result.
|
|
// Harbour: hb_vmLess (hvm.c:4176)
|
|
func (t *Thread) Less() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
cmp, ok := valueCompare(a, b)
|
|
if !ok {
|
|
panic(t.argError("<", a, b))
|
|
}
|
|
t.push(MakeBool(cmp < 0))
|
|
}
|
|
|
|
// LessEqual pops two values, pushes boolean result.
|
|
func (t *Thread) LessEqual() {
|
|
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() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
cmp, ok := valueCompare(a, b)
|
|
if !ok {
|
|
panic(t.argError(">", a, b))
|
|
}
|
|
t.push(MakeBool(cmp > 0))
|
|
}
|
|
|
|
// GreaterEqual pops two values, pushes boolean result.
|
|
func (t *Thread) GreaterEqual() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
cmp, ok := valueCompare(a, b)
|
|
if !ok {
|
|
panic(t.argError(">=", a, b))
|
|
}
|
|
t.push(MakeBool(cmp >= 0))
|
|
}
|
|
|
|
// --- 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() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
if !a.IsLogical() || !b.IsLogical() {
|
|
panic(t.argError(".AND.", a, b))
|
|
}
|
|
t.push(MakeBool(a.AsBool() && b.AsBool()))
|
|
}
|
|
|
|
// Or pops two values, pushes logical OR.
|
|
func (t *Thread) Or() {
|
|
b := t.pop()
|
|
a := t.pop()
|
|
if !a.IsLogical() || !b.IsLogical() {
|
|
panic(t.argError(".OR.", a, b))
|
|
}
|
|
t.push(MakeBool(a.AsBool() || b.AsBool()))
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// Harbour: non-logical values in condition → type error
|
|
panic(t.argError("logical", v))
|
|
}
|
|
|
|
// --- 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
|
|
}
|