Files
five/hbrt/ops_arith.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

415 lines
8.9 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Arithmetic operations for the Five runtime.
// Implements Harbour-compatible type promotion, overflow detection,
// and decimal precision propagation rules.
//
// See docs/harbour-type-system-analysis.md Section 5 for details.
package hbrt
import "math"
// Plus pops two values, pushes their sum.
// Harbour: hb_vmPlus (hvm.c:3285)
//
// Type rules:
// NumInt + NumInt -> NumInt (overflow -> Double)
// Numeric + Numeric -> Double
// String + String -> String (concatenation)
// Date + Numeric -> Date
// Timestamp + Numeric -> Timestamp
func (t *Thread) Plus() {
if t.tryBinaryOp(OpPlus) {
return
}
t.sp -= 2
a := t.stack[t.sp]
b := t.stack[t.sp+1]
t.stack[t.sp+1] = cachedNil
dst := &t.stack[t.sp]
// Fast path: Int + Int (tInt tag — skip tLong to stay branch-light)
if a.Type() == tInt && b.Type() == tInt {
an, bn := int64(a.scalar), int64(b.scalar)
r := an + bn
if (bn >= 0 && r >= an) || (bn < 0 && r < an) {
*dst = MakeNumInt(r)
} else {
*dst = MakeDoubleAuto(float64(an) + float64(bn))
}
t.sp++
return
}
if a.IsNumeric() && b.IsNumeric() {
ad, bd := a.AsNumDouble(), b.AsNumDouble()
dec := maxDec(a.Decimal(), b.Decimal())
*dst = MakeDouble(ad+bd, 255, dec)
t.sp++
return
}
if a.IsString() && b.IsString() {
*dst = MakeString(a.AsString() + b.AsString())
t.sp++
return
}
if a.IsDate() && b.IsNumeric() {
*dst = MakeDate(a.AsJulian() + int64(b.AsNumDouble()))
t.sp++
return
}
if a.IsNumeric() && b.IsDate() {
*dst = MakeDate(int64(a.AsNumDouble()) + b.AsJulian())
t.sp++
return
}
if a.IsTimestamp() && b.IsNumeric() {
days := int64(b.AsNumDouble())
frac := b.AsNumDouble() - float64(days)
ms := int32(frac * 86400000.0)
newJulian := a.AsJulian() + days
newTime := a.AsTimeMs() + ms
if newTime >= 86400000 {
newJulian++
newTime -= 86400000
} else if newTime < 0 {
newJulian--
newTime += 86400000
}
*dst = MakeTimestamp(newJulian, newTime)
t.sp++
return
}
panic(t.argError("+", a, b))
}
// Minus pops two values, pushes their difference.
// Harbour: hb_vmMinus (hvm.c:3401)
func (t *Thread) Minus() {
if t.tryBinaryOp(OpMinus) {
return
}
t.sp -= 2
a := t.stack[t.sp]
b := t.stack[t.sp+1]
t.stack[t.sp+1] = cachedNil
dst := &t.stack[t.sp]
if a.Type() == tInt && b.Type() == tInt {
an, bn := int64(a.scalar), int64(b.scalar)
r := an - bn
if (bn <= 0 && r >= an) || (bn > 0 && r < an) {
*dst = MakeNumInt(r)
} else {
*dst = MakeDoubleAuto(float64(an) - float64(bn))
}
t.sp++
return
}
if a.IsNumeric() && b.IsNumeric() {
ad, bd := a.AsNumDouble(), b.AsNumDouble()
dec := maxDec(a.Decimal(), b.Decimal())
*dst = MakeDouble(ad-bd, 255, dec)
t.sp++
return
}
if a.IsDate() && b.IsDate() {
*dst = MakeLong(a.AsJulian() - b.AsJulian())
t.sp++
return
}
if a.IsDate() && b.IsNumeric() {
*dst = MakeDate(a.AsJulian() - int64(b.AsNumDouble()))
t.sp++
return
}
if a.IsTimestamp() && b.IsTimestamp() {
dayDiff := a.AsJulian() - b.AsJulian()
timeDiff := a.AsTimeMs() - b.AsTimeMs()
if timeDiff != 0 {
*dst = MakeDoubleAuto(float64(dayDiff) + float64(timeDiff)/86400000.0)
} else {
*dst = MakeLong(dayDiff)
}
t.sp++
return
}
panic(t.argError("-", a, b))
}
// Mult pops two values, pushes their product.
// Harbour: hb_vmMult (hvm.c:3510)
// Decimal rule: dec = dec1 + dec2
func (t *Thread) Mult() {
if t.tryBinaryOp(OpMult) {
return
}
t.sp -= 2
a := t.stack[t.sp]
b := t.stack[t.sp+1]
t.stack[t.sp+1] = cachedNil
dst := &t.stack[t.sp]
if a.Type() == tInt && b.Type() == tInt {
an, bn := int64(a.scalar), int64(b.scalar)
if an == 0 || bn == 0 {
*dst = MakeNumInt(0)
t.sp++
return
}
r := an * bn
if r/an == bn {
*dst = MakeNumInt(r)
} else {
*dst = MakeDoubleAuto(float64(an) * float64(bn))
}
t.sp++
return
}
if a.IsNumeric() && b.IsNumeric() {
ad, bd := a.AsNumDouble(), b.AsNumDouble()
dec := a.Decimal() + b.Decimal()
if dec > 255 {
dec = 255
}
*dst = MakeDouble(ad*bd, 255, dec)
t.sp++
return
}
panic(t.argError("*", a, b))
}
// Divide pops two values, pushes the quotient.
// Harbour: hb_vmDivide (hvm.c:3546)
// Always returns Double. Division by zero -> runtime error.
func (t *Thread) Divide() {
if t.tryBinaryOp(OpDivide) {
return
}
b := t.pop()
a := t.pop()
if a.IsNumeric() && b.IsNumeric() {
bd := b.AsNumDouble()
if bd == 0 {
panic(t.divisionByZero())
}
ad := a.AsNumDouble()
t.push(MakeDoubleAuto(ad / bd))
return
}
panic(t.argError("/", a, b))
}
// Modulus pops two values, pushes the remainder.
// Harbour: hb_vmModulus (hvm.c:3608)
// Always returns Double.
func (t *Thread) Modulus() {
b := t.pop()
a := t.pop()
if a.IsNumeric() && b.IsNumeric() {
bd := b.AsNumDouble()
if bd == 0 {
panic(t.divisionByZero())
}
ad := a.AsNumDouble()
t.push(MakeDoubleAuto(math.Mod(ad, bd)))
return
}
panic(t.argError("%", a, b))
}
// Power pops two values, pushes base^exponent.
// Harbour: hb_vmPower
// Always returns Double.
func (t *Thread) Power() {
b := t.pop()
a := t.pop()
if a.IsNumeric() && b.IsNumeric() {
ad, bd := a.AsNumDouble(), b.AsNumDouble()
t.push(MakeDoubleAuto(math.Pow(ad, bd)))
return
}
panic(t.argError("**", a, b))
}
// Negate negates the top of stack.
// Harbour: hb_vmNegate
func (t *Thread) Negate() {
a := t.pop()
if a.IsNumInt() {
t.push(MakeNumInt(-a.AsNumInt()))
return
}
if a.IsDouble() {
t.push(MakeDouble(-a.AsDouble(), a.Length(), a.Decimal()))
return
}
panic(t.argError("negate", a))
}
// Inc increments the top of stack by 1.
// Harbour: hb_vmInc
func (t *Thread) Inc() {
p := t.peekPtr()
if p.IsNumInt() {
*p = MakeNumInt(p.AsNumInt() + 1)
return
}
if p.IsDouble() {
*p = MakeDouble(p.AsDouble()+1, p.Length(), p.Decimal())
return
}
panic(t.argError("++", *p))
}
// Dec decrements the top of stack by 1.
// Harbour: hb_vmDec
func (t *Thread) Dec() {
p := t.peekPtr()
if p.IsNumInt() {
*p = MakeNumInt(p.AsNumInt() - 1)
return
}
if p.IsDouble() {
*p = MakeDouble(p.AsDouble()-1, p.Length(), p.Decimal())
return
}
panic(t.argError("--", *p))
}
// --- Optimized operations (used by generated code) ---
// AddInt adds an integer constant to the top of stack.
// Harbour: hb_xvmAddInt
func (t *Thread) AddInt(n int64) {
p := t.peekPtr()
if p.IsNumInt() {
an := p.AsNumInt()
r := an + n
if (n >= 0 && r >= an) || (n < 0 && r < an) {
*p = MakeNumInt(r)
} else {
*p = MakeDoubleAuto(float64(an) + float64(n))
}
return
}
if p.IsDouble() {
*p = MakeDouble(p.AsDouble()+float64(n), p.Length(), p.Decimal())
return
}
if p.IsDate() {
*p = MakeDate(p.AsJulian() + n)
return
}
panic(t.argError("+int", *p))
}
// LocalAdd adds the top of stack to a local variable, pops the value. Byref-aware.
// Harbour: hb_xvmLocalAdd
func (t *Thread) LocalAdd(n int) {
val := t.pop()
idx := t.localIndex(n)
loc := t.locals[idx]
isRef := loc.Type() == tByref
if isRef {
loc = (*HbRefCell)(loc.ptr).V
}
var result Value
if loc.IsNumInt() && val.IsNumInt() {
r := loc.AsNumInt() + val.AsNumInt()
if (val.AsNumInt() >= 0 && r >= loc.AsNumInt()) || (val.AsNumInt() < 0 && r < loc.AsNumInt()) {
result = MakeNumInt(r)
} else {
result = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val.AsNumInt()))
}
} else if loc.IsNumeric() && val.IsNumeric() {
dec := maxDec(loc.Decimal(), val.Decimal())
result = MakeDouble(loc.AsNumDouble()+val.AsNumDouble(), 255, dec)
} else if loc.IsString() && val.IsString() {
result = MakeString(loc.AsString() + val.AsString())
} else {
panic(t.argError("+=", loc, val))
}
if isRef {
(*HbRefCell)(t.locals[idx].ptr).V = result
} else {
t.locals[idx] = result
}
}
// LocalAddInt adds an integer constant directly to a local variable. Byref-aware.
// Harbour: hb_xvmLocalAddInt (fused PUSHINT + PLUS + POPLOCAL)
func (t *Thread) LocalAddInt(n int, val int64) {
idx := t.curFrame.localBase + n - 1 // inline index
loc := t.locals[idx]
isRef := loc.Type() == tByref
if isRef {
loc = (*HbRefCell)(loc.ptr).V
}
var result Value
if loc.IsNumInt() {
r := loc.AsNumInt() + val
if (val >= 0 && r >= loc.AsNumInt()) || (val < 0 && r < loc.AsNumInt()) {
result = MakeNumInt(r)
} else {
result = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val))
}
} else if loc.IsDouble() {
result = MakeDouble(loc.AsDouble()+float64(val), loc.Length(), loc.Decimal())
} else if loc.IsDate() {
result = MakeDate(loc.AsJulian() + val)
} else {
panic(t.argError("+int", loc))
}
if isRef {
(*HbRefCell)(t.locals[idx].ptr).V = result
} else {
t.locals[idx] = result
}
}
// --- Helpers ---
func maxDec(a, b uint16) uint16 {
if a == 255 || b == 255 {
return 255 // HB_DEFAULT_DECIMALS
}
if a > b {
return a
}
return b
}
func (t *Thread) divisionByZero() *HbError {
return &HbError{
Description: "division by zero",
Operation: "/",
SubSystem: "BASE",
GenCode: 1340, // EG_ZERODIV
}
}