Thank you so much for this link -- I have finally satisfied my curiosity after 40 years!

I've spent some time today looking into the floating-point routines.

Some parts are quite nice, others are a bit... clumsy.

Something that surprised me was that they use 5 constants (0, 1, 0.5, pi/2, 10 -- in that order) stored in some sort of ad hoc packed format. That saves all of 7 bytes but the code to handle that takes more. Why?!

There is a routine that takes a constant index (0-4) in the A register and stores the (unpacked) floating-point constant on the calculator stack. It does that by skipping the constants before the desired one -- and that is done by performing a dummy unpack on them (by unpacking them to a ROM address).

See:
https://skoolkit.ca/disassemblies/rom/hex/asm/341B.html

[ah, the mystery is solved later -- see below]

There are low-level routines for addition, multiplication, division, abs/neg (they share most of their code), and some low-level stack manipulation.

There are special-cased comparison operations for 0 and true.

OR and AND build on those special-cased comparison operations.

Subtraction is built on neg + addition.

sin/cos/tan/arcsin/arccos/arctan/exp/ln perform argument validity checking, argument reduction, and then evaluate a Chebyshev polynomial (and clean up the result). There is a special "series evaluator" function for that + some of the routines share their argument reduction code.

The power operator is built on ln and exp.

The square root operator is built on the power operator (x^0.5).

Most of the internal floating-point code use a compact VM to represent calculations involving other, lower-level floating-point code. We enter the VM with the RST $28 instruction (a single-byte call to address 28h) and then a stream of single-byte opcodes follow, with the last one being a terminator. Each opcode encodes a call to a single floating-point operation. Some have immediate bytes: the opcode for pushing (packed) immediates to the calculator stack, the opcode for invoking the Chebyshev series evaluator (it is followed by a number of packed constants), and the opcodes for a loops and branches (followed by a single displacement byte for two of them). The loop instruction is basically a DJNZ, the unconditional branch is a "JR", and the conditional branch is a "JR true" where "true" is taken to be a non-zero value at the top of the calculator stack (it doesn't get popped).

So, it turns out that all floating-point constants in the ROM are in the packed format. That solves the mystery above.

The Spectrum uses a 5-byte floating-point format with a full byte for the exponent -- this is much easier to work with on a Z80 than the 32-bit IEEE format (because the Z80 is really not good at bit fiddling across bytes). It also encodes a 16-bit integer format into the same 5 bytes, which speeds up most parts of a typical BASIC program (+ avoids weird rounding errors). The floating-point package handles both formats -- the outermost routines "unpack" the integers to real floating-point if necessary, the innermost routines only work on real floating-point numbers.

---

There seems to be a bug in the comments for the LN function:

https://skoolkit.ca/disassemblies/rom/hex/asm/3713.html

It says:

3717 DEFB $00 jump_true to VALID: X
3718 DEFB $04 multiply: X

It should have said:

3717 DEFB $00 jump_true to VALID: X
3718 DEFB $04

because the 4 is not a multiply opcode, it is just the displacement immediate for the jump_true opcode.

---

Most VM opcodes are treated simple enough: just use the opcode as in index into a jump table. If bit 7 is set, we have a special case: The real opcode is in bits 6-5 and there is a parameter encoded in bits 4-0. This is used for the opcodes that push a known constant (0, 1, 0.5, pi/2, 10) to the stack, it is used for the series evaluator, and for pushing and popping any of 6 internal floating-point "registers" (used for FOR-NEXT loops and various other routines). The series evaluator uses the parameter in the low nibble to indicate how many constants to use: 6, 8, or 12.