Divergences from MBASIC 5.21 still outstanding¶
Found by cross-checking basic/dev/tests_with_results/ against the real binary
under cpmemu, and by a sweep of the whole error catalogue. Everything here was
measured against com/mbasic.com, and most of it was also traced back to the
reconstructed 5.21 assembler in
/home/wohl/src/mbasic2025/mbasic_521/mbasic_src/.
What has been fixed is in
TESTS_VERIFIED_AGAINST_BINARY.md: PRINT
USING, the TRON trace, MOD and \, operator precedence, zero-trip FOR, and
untrapped error reporting. This file is the remainder.
1. Errors that are still not detected¶
The reporting is right now - <message> in <line>, no ?, program stops - and
25 provocations match the binary exactly. What is left is conditions we simply
do not notice. Each prints nothing at all where the binary raises:
| provocation | binary says |
|---|---|
A$=STRING$(200,65) then twelve nested concatenations |
String formula too complex in 20 |
DIM A$(300) filled with 200-character strings |
Out of string space in 20 |
FIELD #1, 100 AS A$ on a 16-byte record |
FIELD overflow in 20 |
OPEN "O",1,"A:B:C*?.X Y" |
Too many files in 10 |
| a line longer than the 255-byte buffer | Line buffer overflow in 10 |
LOAD of a file with a line that has no number |
Direct statement in file |
| running off the end of the program inside a handler | No RESUME in <last line> |
One more is wrong rather than missing:
PRINT 1E38*1E38gives1E+76. On the binary a floating-point overflow is a warning like division by zero - it printsOverflow, substitutes machine infinity and carries on. Ours has no MBF range to overflow out of, which is the deliberate IEEE decision in section 10; the message is the part that is missing. The integer overflows (C% = 40000,40000 \ 2,CINT(40000)) are fatal and do now match.
src/error_codes.py still carries nine codes 5.21 has not got - 24, 25, 68-72,
75 and 76, all Unprintable error on the binary. Harmless, since nothing
produces them, but they are not 5.21.
FRCINT's 16-bit range check is applied to \ and MOD operands and to %
assignment, but not yet everywhere the binary applies it: A(40000),
TAB(1E38), STRING$(40000,65), LEFT$("AB",1E38), PEEK(70000),
MKI$(40000), SPACE$(40000), HEX$(1E38), OCT$(1E38) are all Overflow
there.
2. ON ERROR, and what happens around a handler¶
The common path works - a trapped error reaches the handler with the right ERR and ERL, and RESUME NEXT lands in the right place. The edges do not:
ON ERROR GOTO 0inside a live handler should disable the handler and re-raise the pending error, reported at the line that originally failed. We only disarm and carry on.ON ERROR GOTO <line that does not exist>isUndefined line numberat theON ERRORstatement on the binary - checked when it executes, not when it fires.- A handler that ends with
ENDmakes us re-raise the original error at the END line. The binary just printsOk. - An error raised inside a handler crashes us with a Python
AttributeErrorabouton_error_goto. Break in <line>is printed twice, and CONT re-prints it instead of resuming.
3. No Ok prompt¶
The binary prints Ok after every direct statement and after every program
stop, error or not. We print it only when a program finishes. Typing three
commands at the prompt gives
binary Illegal function call / Ok / RETURN without GOSUB / Ok / Type mismatch / Ok
ours Illegal function call / RETURN without GOSUB / Type mismatch
It is a visible piece of the 5.21 feel and the messages themselves now match, so this is the largest remaining difference in an interactive session. It is also a change to how every UI looks, which is why it is here rather than done.
4. Errors reach the user through print(), not through the I/O handler¶
The wording is now consistent everywhere - see
TESTS_VERIFIED_AGAINST_BINARY.md - but
src/interactive.py still writes roughly forty of its messages with a bare
print(...) rather than self.io.output(...). In the CLI that is the same
thing; in curses, Tk and the web UI it goes to a terminal nobody is looking at.
The UI backends each carry their own copy of the command handlers - cmd_load,
cmd_merge, cmd_renum and the rest exist four times over, once per backend,
which is why the same message had to be fixed in four places. Collapsing them
onto one implementation that reports through the I/O handler would fix both
problems at once, and is the larger piece of work behind this entry.
5. Reserved words followed by a type suffix¶
NAME$, LEN%, FOR$, TO%, ERR, ERL are all syntax errors on the real
binary, and we accept them. Two of the tests here were written using NAME$
and died at that line on the binary; they have been renamed to NM$, but the
lexer rule is untouched.
MBASIC's CRUNCH (bintrp.mac, the tstanm call around line 2504) treats a
reserved word as a keyword only when the character after it is not a letter,
not a digit and not .. That single rule gives:
NAME$ FOR$ LEN% TO% ERR ERL Syntax error
TOTAL NAMES$ FORM COST NAME.X fine - all accepted
FORX=1TO3 Syntax error (TO is followed by 3)
So the folklore that a variable name may not contain a reserved word is false for 5.21; what matters is only what follows the word.
Our lexer (src/lexer.py:250-267) does maximal munch, pulls the trailing
suffix into the identifier, and then looks the whole string up in KEYWORDS.
MID$ and STR$ only work because those entries are stored with the dollar.
Two traps for anyone implementing the real rule:
INPUT$(n)is legal on 5.21 and must keep working.INPUT$is not in the reserved list at all -INPUTcrunches and the$stays an ordinary character.TIME$is not reserved in the CP/M build, andLEFTandCHRwithout the dollar are ordinary variables.
6. Syntax errors are found at the wrong time¶
We reject a bad line when it is entered, print the complaint there, store the
line anyway, and then silently skip it when the program runs. The binary
crunches the line at entry without complaint and reports Syntax error in 10
at RUN - after which it opens the line editor on the offending line, which is
its own piece of behaviour we do not have.
7. The compiler backends are out of step with the interpreter¶
The precedence fix is in the parser, so every backend gets it. The operator semantics are not:
src/codegen_js_backend.py-\now truncates rather than flooring, but neither\nor MOD rounds its operands to integers first, and neither range-checks them.7.6 MOD 3is 2 in the interpreter and 1.6 here.src/codegen_backend.py- the C backend has no entry for\or MOD in its operator map at all (_generate_binary_op, around line 3324), so it emits(a ? b). This predates the current work.src/codegen_js_backend.py_match_for_next(line ~236) pairs NEXT with FOR by variable name and jumps to a line rather than a statement, so it cannot reproduceNEXT J,NEXT J,Ior a mid-line skip. Its_start_ok(line ~2091) is(_step > 0) ? (v <= end) : (v >= end), which gets STEP 0 wrong in the opposite direction from the bug fixed in the interpreter.
8. serialize_expression does not bracket under a unary minus¶
Found while checking the precedence change did not break RENUM. Pre-existing, and unrelated to any of the above:
-(A + B) comes back out as -A + B
-(A * B) comes back out as -A * B
_serialize_operand in src/ui/ui_helpers.py is only reached from the binary
path; UnaryOpNode emits its operand with no brackets whatever it is. Every
binary case round-trips - tests/regression/serializer/test_operator_brackets.py
covers 34 of them - so the fix is to give the unary path the same treatment,
bracketing an operand whose precedence is looser than unary minus.
-(A \ B) happens to be safe (truncating division is odd-symmetric, so
-(a \ b) and (-a) \ b agree), but -(A + B) is a plain wrong answer.
9. Statements where we are ahead of 5.21¶
Not bugs so much as decisions to record - each one is a place where a program that works here would not work on the real thing.
OPEN "A"(append) is not a 5.21 file mode; the binary answersBad file mode.test_file_io.basuses it, which is why that test is not binary-verified.MERGEreturns to command level on 5.21 - the rest of the program does not run. We carry on. (The summary line MERGE used to print is now suppressed when a running program merges, which is what the binary does.)
10. Deliberate, and staying that way¶
MBASIC's arithmetic is Microsoft Binary Format and ours is IEEE - a project
decision, see src/number_format.py and the note in
docs/dev/TESTS_VERIFIED_AGAINST_BINARY.md. These differ and should:
CDBL of a single 3.14 real 3.139999866485596 ours 3.140000104904175
5 ^ 2 = 25 real false (EXP/LOG gives 24.999998)
VAL("99.99") = 99.99 real false, and by a different amount than ours
The MBF range goes with it: 1E38*1E38 overflows there and does not here. Machine infinity, 1.70141E+38, is nonetheless reproduced where it is observable - a float divide by zero yields it, as it does on the binary.