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