What is happening
The super-summary is that INT is calling routines which round instead of truncating when converting from double-precision to single-precision.
For error 5A, when the input is double-precision (type flag NTF=8), it first converts the value to single-precision, and that conversion routine (CSNG or equivalent) performs rounding to the nearest single-precision representable value. Only then does it apply the truncation to integer, leading to incorrect results for values like 2.9999999# (which rounds up to 3.0 in single precision before INT truncates it to 3).
This behavior violates the expected floor semantics of INT for positive numbers (greatest integer ≤ value).
For error 5B, the bug occurs in the double-precision to single-precision conversion routine at address $0A00, which is called as part of preparing the argument for the INT function (via $0D18 in INTFUN at $0A80). This conversion adds 0.5 ulp (unit in the last place) for rounding to nearest, which can cause values like 32767.9999# (just below 32768) to round up to exactly 32768.0 in single-precision.
After this conversion, the INT logic truncates the fractional part (via $0AFB), but since the value is now exactly 32768.0, it remains 32768.0. Then, when converting the resulting single-precision float to a 16-bit signed integer (via CONVFI at $0B37, called from $1F7C after INTFUN), the exponent check (CP $90 at $0B39) detects the exponent as $90 (144, corresponding to magnitude >= 32768), triggering an overflow error jump to $0B5B.
Location in the ROM
The INT function entry point is at ROM address 0B37H (decimal 2871). However, the core logic (including the buggy handling) is in a nearby routine starting around 0A80H (depending on the disassembly version; some label it differently, but the functionality is the same). The problematic part is a conditional call that delegates double-precision inputs to a conversion subroutine (at approximately 0AB9H or CSNG at 0AB1H in some disassemblies), which introduces the rounding.
Relevant Disassembled Code
0A80
LD HL,(4121H) 2A2141
Load LSB of single-precision value from FACLO (working register area)
0A83
RET M F8
Return if already integer (sign bit check)
0A84
JP Z,0AF6H CAF60A
Jump to type mismatch (TM) error if invalid type (e.g., string)
0A87
CALL NC,0AB9H D4B90A
*** BUG HERE: If double-precision (NC flag set based on type), call conversion routine (CSNG-like) which rounds to single-precision ***
0A8A
LD HL,07B2H 21B207
Load overflow error handler address
0A8D
PUSH HL E5
Push for error return
0A8E
LD A,(4124H) 3A2441
Get exponent from FAC
0A91
CP 90H FE90
Compare to exponent bias (144 decimal) for integer range check
0A93
JR NC,0AA3H 300E
If exponent >= 90H (value too large), handle overflow
0A95
CALL 0AFBH CDFB0A
Convert to integer (truncate fractional part after any prior conversion)
0A98
EX DE,HL EB
Swap to HL for result
0A99
POP DE D1
Pop return address
0A9A
LD (4121H),HL 222141
Store integer result back to FACLO
0A9D
LD A,02H 3E02
Set type flag to integer (NTF=2)
0A9F
LD (40AFH),A 32AF40
Store in VALTYP (system type flag)
0AA2
RET C9
Return with integer result
Key Buggy Line:
0A87: D4B90A CALL NC,0AB9H
- This checks the carry flag (based on prior type test via RST 20H or equivalent).
- If the value is double-precision, it calls the subroutine at 0AB9H (a sign/compare or CSNG entry point), which converts double to single-precision with rounding to nearest (standard IEEE-like behavior in TRS-80 floating-point, adding 0.5 in mantissa before truncating lower bits).
- This rounding happens before truncation, causing values like 2.9999999# to become 3.0 in single-precision, and then INT(3.0) = 3.
- For single-precision or integer inputs, no call is made, and direct truncation occurs (correct behavior).
Subroutine Called (Double to Single Conversion)
The called subroutine (around 0AB9H, often labeled CSNG or CONSGN) handles the conversion and rounding:
- It unpacks the double-precision mantissa (8 bytes) from DFACLO (0x411D).
- Scales and shifts the mantissa to fit single-precision (4 bytes).
- Adds a rounding adjustment (implicit +0.5 in the lower bits) before discarding the least significant bits, causing round-to-nearest.
- Stores the result in FACLO (0x4121) as single-precision, setting NTF=4.
- Example partial code (from disassemblies):
0AB9
Unpack double mantissa (rounding/truncation here via shift)
- The unpack at 0D69H performs the shift and discards fractional bits, but with a prior adjustment that rounds instead of pure truncate.
⬑ Click to Enlarge
⬑ Click to Enlarge
The Fix:
Skip the CALL to the rounding conversion when double-precision. Instead, proceed directly to the truncation logic (which works on the FACLO single-precision area, but we can adjust the mantissa/exponent handling if needed).
An easy/dirty way to do this is to rReplace the conditional CALL to conversion with a NOP or unconditional jump over it, forcing direct truncation.
POKE 2695,00 : POKE 2696,00 : POKE 2697,00
A more precise fix would be to add code to subtract 1 from the integer part if fractional part > 0 for positive numbers (full floor semantics), but since original INT is truncate-toward-zero for positive, bypassing rounding achieves the main goal.
Another fix would be to rewrite the code to load DFACLO, adjust exponent/mantissa, truncate fractional bits
Another fix would be to find some place in the ROM with 6 bytes free and use:
nnnn LD A,4 (3E 04)
nnnn LD ($40A4),A (32 A4 40); Set VALTYP to 4 (single-precision)
nnnn RET (C9)
and then replace the "CALL NC,0AB1H" at 0A87H with "CALL NC,nnnn"