// 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() { b := t.pop() a := t.pop() // Fast path: Int + Int if a.IsNumInt() && b.IsNumInt() { an, bn := a.AsNumInt(), b.AsNumInt() r := an + bn // Overflow detection (Harbour pattern) if (bn >= 0 && r >= an) || (bn < 0 && r < an) { t.push(MakeNumInt(r)) } else { t.push(MakeDoubleAuto(float64(an) + float64(bn))) } return } // Numeric + Numeric -> Double if a.IsNumeric() && b.IsNumeric() { ad, bd := a.AsNumDouble(), b.AsNumDouble() dec := maxDec(a.Decimal(), b.Decimal()) t.push(MakeDouble(ad+bd, 255, dec)) return } // String + String -> concatenation if a.IsString() && b.IsString() { t.push(MakeString(a.AsString() + b.AsString())) return } // Date + NumInt -> Date (add days) if a.IsDate() && b.IsNumInt() { t.push(MakeDate(a.AsJulian() + b.AsNumInt())) return } if a.IsNumInt() && b.IsDate() { t.push(MakeDate(a.AsNumInt() + b.AsJulian())) return } // Timestamp + Numeric -> Timestamp 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 } t.push(MakeTimestamp(newJulian, newTime)) return } panic(t.argError("+", a, b)) } // Minus pops two values, pushes their difference. // Harbour: hb_vmMinus (hvm.c:3401) func (t *Thread) Minus() { b := t.pop() a := t.pop() // Fast path: Int - Int if a.IsNumInt() && b.IsNumInt() { an, bn := a.AsNumInt(), b.AsNumInt() r := an - bn if (bn <= 0 && r >= an) || (bn > 0 && r < an) { t.push(MakeNumInt(r)) } else { t.push(MakeDoubleAuto(float64(an) - float64(bn))) } return } // Numeric - Numeric -> Double if a.IsNumeric() && b.IsNumeric() { ad, bd := a.AsNumDouble(), b.AsNumDouble() dec := maxDec(a.Decimal(), b.Decimal()) t.push(MakeDouble(ad-bd, 255, dec)) return } // Date - Date -> Long (difference in days) if a.IsDate() && b.IsDate() { t.push(MakeLong(a.AsJulian() - b.AsJulian())) return } // Date - NumInt -> Date if a.IsDate() && b.IsNumInt() { t.push(MakeDate(a.AsJulian() - b.AsNumInt())) return } // Timestamp - Timestamp -> Double or Long if a.IsTimestamp() && b.IsTimestamp() { dayDiff := a.AsJulian() - b.AsJulian() timeDiff := a.AsTimeMs() - b.AsTimeMs() if timeDiff != 0 { t.push(MakeDoubleAuto(float64(dayDiff) + float64(timeDiff)/86400000.0)) } else { t.push(MakeLong(dayDiff)) } 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() { b := t.pop() a := t.pop() if a.IsNumInt() && b.IsNumInt() { an, bn := a.AsNumInt(), b.AsNumInt() if an == 0 || bn == 0 { t.push(MakeNumInt(0)) return } r := an * bn if r/an == bn { t.push(MakeNumInt(r)) } else { t.push(MakeDoubleAuto(float64(an) * float64(bn))) } return } if a.IsNumeric() && b.IsNumeric() { ad, bd := a.AsNumDouble(), b.AsNumDouble() dec := a.Decimal() + b.Decimal() if dec > 255 { dec = 255 } t.push(MakeDouble(ad*bd, 255, dec)) 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() { 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() { a := t.pop() if a.IsNumInt() { t.push(MakeNumInt(a.AsNumInt() + 1)) return } if a.IsDouble() { t.push(MakeDouble(a.AsDouble()+1, a.Length(), a.Decimal())) return } panic(t.argError("++", a)) } // Dec decrements the top of stack by 1. // Harbour: hb_vmDec func (t *Thread) Dec() { a := t.pop() if a.IsNumInt() { t.push(MakeNumInt(a.AsNumInt() - 1)) return } if a.IsDouble() { t.push(MakeDouble(a.AsDouble()-1, a.Length(), a.Decimal())) return } panic(t.argError("--", a)) } // --- 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) { a := t.pop() if a.IsNumInt() { an := a.AsNumInt() r := an + n if (n >= 0 && r >= an) || (n < 0 && r < an) { t.push(MakeNumInt(r)) } else { t.push(MakeDoubleAuto(float64(an) + float64(n))) } return } if a.IsDouble() { t.push(MakeDouble(a.AsDouble()+float64(n), a.Length(), a.Decimal())) return } if a.IsDate() { t.push(MakeDate(a.AsJulian() + n)) return } panic(t.argError("+int", a)) } // LocalAdd adds the top of stack to a local variable, pops the value. // Harbour: hb_xvmLocalAdd func (t *Thread) LocalAdd(n int) { val := t.pop() idx := t.localIndex(n) loc := t.locals[idx] if loc.IsNumInt() && val.IsNumInt() { r := loc.AsNumInt() + val.AsNumInt() if (val.AsNumInt() >= 0 && r >= loc.AsNumInt()) || (val.AsNumInt() < 0 && r < loc.AsNumInt()) { t.locals[idx] = MakeNumInt(r) } else { t.locals[idx] = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val.AsNumInt())) } return } if loc.IsNumeric() && val.IsNumeric() { dec := maxDec(loc.Decimal(), val.Decimal()) t.locals[idx] = MakeDouble(loc.AsNumDouble()+val.AsNumDouble(), 255, dec) return } if loc.IsString() && val.IsString() { t.locals[idx] = MakeString(loc.AsString() + val.AsString()) return } panic(t.argError("+=", loc, val)) } // LocalAddInt adds an integer constant directly to a local variable. // Harbour: hb_xvmLocalAddInt (fused PUSHINT + PLUS + POPLOCAL) func (t *Thread) LocalAddInt(n int, val int64) { idx := t.localIndex(n) loc := t.locals[idx] if loc.IsNumInt() { r := loc.AsNumInt() + val if (val >= 0 && r >= loc.AsNumInt()) || (val < 0 && r < loc.AsNumInt()) { t.locals[idx] = MakeNumInt(r) } else { t.locals[idx] = MakeDoubleAuto(float64(loc.AsNumInt()) + float64(val)) } return } if loc.IsDouble() { t.locals[idx] = MakeDouble(loc.AsDouble()+float64(val), loc.Length(), loc.Decimal()) return } if loc.IsDate() { t.locals[idx] = MakeDate(loc.AsJulian() + val) return } panic(t.argError("+int", loc)) } // --- 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 } }