Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
368 lines
8.4 KiB
Go
368 lines
8.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
|
|
}
|
|
// 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
|
|
}
|