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.
Each open difference has a GitHub issue (#30 to #57). The issue has a reproduction on the binary, the cause in our code and a proposed fix. When an issue is closed, change its entry here to "Done", or remove the entry.
1. Errors that were not detected - mostly done¶
The reporting is right now - <message> in <line>, no ?, program stops - and
25 provocations match the binary exactly. The conditions that we did not
notice before are now detected:
Done (GitHub issues 30 to 33, docs/dev/STRING_SPACE_AND_FILE_ERRORS.md):
- Done (#30): a string expression that needs more than 10 string temporaries
gives
String formula too complex(error 16), as on the binary. The test recipe is a nest of elevenCHR$(65)+(...). The old recipe (STRING$(200,65)and twelve concatenations) givesString too long. The body of a DEF FN function is not counted. - Done (#31):
Out of string space(error 14) when the live strings do not fit in the string space. The space is a byte budget (39168 at start-up, as FRE reports), not a memory model, so the error comes a little later than on the binary.CLEAR ,nandCLEAR ,,nchange the budget, andFREreports the string space less the live strings. Only a literal (classProgramString) is free. A result ofLEFT$,RIGHT$,MID$or+counts, also when it equals the whole literal. ADEF FNthat returns a variable or a parameter counts (a copy on the binary); aDEF FNwith a literal body (also through a chain ofDEF FNcalls), and a string read fromDATA, are free. - Done (#32):
OPEN "R"keeps the record length (0 to 128, default 128),FIELDgivesFIELD overflowwhen the widths are more than the record length, andGETandPUTuse the record length. The binary binds the variables before the one that overflows. The interpreter changes nothing. - Done (#33):
Bad file nameandToo many filesfor names that CP/M cannot hold, in OPEN, KILL, NAME, LOAD, SAVE, MERGE, CHAIN and RUN in a program. A name with a slash or backslash passes as a host path, and the settingcpm_file_namesturns the check off. A command typed at the prompt is not checked. - The JavaScript and C backends do not raise any of these four errors.
Done (#34, #35, #36): LOAD and MERGE of a file stop reading at the first bad
line, as the binary does. The bad line is a line over 255 bytes (Line buffer
overflow in <number>, 0 if the line has no number), a line with no number
(Direct statement in file) or a bare number of a missing line (Undefined
line number). The message is bare. The lines read before it stay: LOAD of a
file with a bad first line leaves an empty program, and MERGE keeps the old
program. A bare number of a missing line typed at the prompt also prints
Undefined line number. Inside a running program the binary prints the bare
message, does not run any ON ERROR handler and stops the program; ours does
the same. RUN "file" of a file with a bad line stops after the message and
does not run. The Tk and curses UIs print the same
message. The web UI loads files through the editor and does not use the loader.
CHAIN and CHAIN MERGE use the same loader (cmd_chain in
src/interactive.py), so they follow the same rules. Measured: after the bad
line the program stops with the bare message. CHAIN leaves the lines read
before the bad line (an empty program for a bad first line). CHAIN MERGE
keeps the old program and the lines merged before the bad line. Before this,
cmd_chain read the file with its own loop and ignored the bad lines, and
CHAIN MERGE of a bare number of a missing line looped for ever. The curses
and Tk cmd_load and cmd_merge now return True after a read abort, so a
running program stops there too. Tests:
tests/regression/integration/test_chain_file_errors.py. The curses and Tk UIs
now have a cmd_chain (chain_into_program in src/ui/ui_helpers.py); it
loads the file and starts the program with cmd_run, as RUN "file" does.
After a read abort in the CLI (LOAD, MERGE, CHAIN, CHAIN MERGE) the
variables, arrays and FOR/GOSUB state are cleared and CONT gives
Can't continue (measured). CHAIN with a start line now begins at that line;
a start line that the new program lacks prints a bare Undefined line number.
CHAIN MERGE with DELETE a-b now parses the range (before, 100-110 was read
as the number -10 and the range was lost, so the merge looped for ever) and
deletes those lines from the old program before the merge, even the CHAIN line.
The file is opened first: a missing file deletes nothing. A missing first or
last line of the range is Illegal function call at the CHAIN line, with
nothing deleted. CHAIN MERGE without ALL clears the variables (only COMMON
variables pass on), as CHAIN does. The web UI uses the same order and rules.
DELETE a- (open range) is Illegal function call on the binary; ours is a
syntax error. The curses, Tk and web read-abort paths do not clear the
variables yet.
A floating-point overflow is now reported. The binary prints a bare Overflow
(no line number, no Ok). It substitutes machine infinity with the sign of the
true result, and the program goes on. With ON ERROR armed it is ERR 6.
The interpreter and the JavaScript backend do the same for a result that is
infinite in the IEEE format of its precision (binary32 for single, binary64 for
double). Examples are 10^40, 1.7E38*10, EXP(100), 1E38*1E38,
CSNG(1D39), A! = 1D39 and VAL("1E400").
A literal that is too big, such as 1E39, gets Overflow when the line is
entered. Machine infinity takes its place. 0^-1 is a non-fatal
Division by zero. These are Illegal function call: a negative base with a
fractional exponent, SQR of a negative number, and LOG of zero or a
negative number. CINT(40000) is Overflow.
What is still different:
- The MBF range is not emulated (section 10).
PRINT 2^127,EXP(88)andEXP(100#)are legal here andOverflowon the binary.1D300/1D-300isOverflowhere andDivision by zeroon the binary, because MBF cannot hold 1D-300.1D300^2andEXP(1000#)print a single-precision machine infinity on the binary and a double one here. - MBF has no denormal numbers. A result below about 1.5E-39 is 0 on the
binary (
PRINT EXP(-100)andPRINT 1E-20*1E-20print 0). IEEE gives a denormal number here (3.78351E-44). This follows from the IEEE decision in section 10. PRINT USINGwith^^^^and a value that is close to a power of ten (such as 9.99999E30): the binary scales the value by a product of powers of ten in MBF. Two effects follow. (1) The scaled value can round to exactly 1, and then the binary does renormalize. With a single-precision value,"##.#^^^^"gives1.0E+22for 9.99999E21 and1.0E-20for 9.99999E-21, and"#.##^^^^"gives0.10E-19for 9.99999E-21. We give10.0E+21,10.0E-21and1.00E-20. This shows for exponents of about 21 to 37 and about -21 to -39. (2) The binary truncates the scaled value when the exponent is about 12 to 26 ("##.#^^^^"gives9.9E+12for 9.95E12 and9.9E+26for 9.95E26). We round and give10.0E+12. Double values differ in the same way ("#.##^^^^"with 9.99999999999999D+99 gives0.17D+39on the binary, a value in the MBF range). Issue #22 is only partly fixed: the case of a carry that is not caused by the MBF scaling agrees. We do not emulate the MBF multiplication.- The sign of machine infinity after a double division by zero is erratic on the binary (it depends on what was computed before). We use the sign of the dividend.
- Done (#38):
LOAD,MERGEand the command-line load printOverflowfor each numeric literal that is too big (such as1E39), while they read the file, as a typed line does.LISTshows the text as typed. - Done (#39): a line with an
&Hconstant of more than four digits, or an&Oconstant above 65535, is refused withOverflowand not stored. A refused replacement leaves the old line.LOADandMERGEstop reading the file at such a line, as the binary does (the lines after it are not loaded). The JavaScript compiler stops with a compilation error, and the compilation error stays. - Done (#39, DATA): DATA text is not crunched, so a DATA line with
&O9,&O8or&O19is stored, listed and loaded.READ A$gives a numeric DATA item as it was typed (&H12345,&O999,1E39,1.50). Outside DATA the refusal stays. The lexer splits the rest of a DATA statement into raw items (up to a comma or a colon). A clean number goes through the number reader; every other item (1 2,12abc,&OA,&H12G,X+1,(1),PRINT) is a string with the case kept and the trailing spaces dropped, as on the binary. An empty item is an empty string. A signed item such as+3reads as+3intoA$. The JavaScript compiler does the same. Text after a closing quote ("q"x,"q" x,"q"1,"q"'c) is a Syntax error at the DATA line on the binary, when READ reaches the item.READ A$stores the string first, then the error is raised and the DATA pointer goes back to where the READ began, so the next READ gives the same item and the same error. Spaces before a comma, a colon or the end of the line are fine. The lexer marks the item (bad_tailon the token and onStringNode); the interpreter and the JavaScript compiler (_read_check) raise the error after the store. Test:tests/regression/integration/test_data_quote_tail.py. READ into a number variable parses the text of an unquoted item at READ time, as the binary does (data_text_to_numberinsrc/interpreter.py, and the same code in the JavaScript compiler). Spaces inside the digits are ignored (1 2is 12),1Eis 1, a type suffix at the end is accepted, and&H,&Oand&take digits only. A bad item is a Syntax error reported at the DATA line (abc,12abc,1.5.5,&HG, a quoted item, a sign before&);&O8,&O19and&9are reported at the READ line; a value over 65535 is an Overflow at the READ line.ERLis the DATA line, andRESUME NEXTgoes on after the READ, and the DATA pointer goes back to where the READ statement began, so the items that the same READ already read are read again, and the bad item is the next item to read (READ B$after the trapped error gets it).ON ERROR GOTO 0inside the handler reports the DATA line, as ERL does (ErrorInfo.report_pc). Test:tests/regression/integration/test_entry_time_forms.py. - Done: a line number stops at 6553. MBASIC takes no more digits once the value
is 6553 or more, so
70000 PRINT 1is line 7000 with the text0 PRINT 1, and a bare70000is line 7000 with the text0.65529is a line number. The prompt,LOADandMERGEdo this (src/line_number_split.py). - Done (#40): the text of an
OverfloworDivision by zeromessage that does not stop the program goes through the width logic of PRINT, one character at a time, in the interpreter and in the JavaScript backend. WithWIDTH 40and the cursor near the right edge the word is split (Overflo/w), as the binary does. Test:tests/regression/integration/test_notice_width_wrap.py. - Done (#41): the JavaScript backend stops with
<message> in <line>with no?, as the interpreter does.ERROR nuses the message of code n. A fatalRETURNin a program with noGOSUBisRETURN without GOSUB in <line>(it was a JavaScript ReferenceError).STOPprintsBreak in <line>, also inside a handler, and starts a new line first when the cursor is not in column 0. The interpreter does the same now.RESUMEto a line that does not exist is error 8 at theRESUME, so a handler that is still armed catches it again, as the binary does (it was fatal). AnERROR 9inside a handler printsSubscript out of range in <line>, in the interpreter and in the JavaScript backend. Test:tests/regression/integration/test_js_fatal_error_line.py.
Done (#42): src/error_codes.py no longer carries the nine codes 5.21 has not
got - 24, 25, 68-72, 75 and 76. ERROR 24 prints Unprintable error, as the
binary does, and ERR stays 24. The JavaScript backend prints Unprintable
error in 10 too.
Done for the interpreter (#43): the
interpreter applies FRCINT's 16-bit range check, and CONINT's second test,
wherever the binary does. An integer argument is rounded and must fit
-32768..32767, or the result is Overflow. A byte argument (LEFT$,
RIGHT$, MID$, SPACE$, STRING$, CHR$, the start of INSTR, INP,
OUT, the value of POKE, ON...GOTO, file numbers and INPUT$) must then
be 0 to 255, or the result is Illegal function call (so LEFT$("AB",300)
and SPACE$(256) stop with an error, as on the binary). A negative array
subscript or DIM bound is Illegal function call. TAB, SPC, MKI$,
RANDOMIZE and WIDTH check 16 bits. PEEK and the address of POKE
accept -32768 to 65535; PEEK still returns a random byte.
The same checks cover these statements:
ERROR n: 0 and above 255 areIllegal function call, 5.6 rounds to 6.- The
MID$statement: start and length are checked. A start of 0, or a start beyond the end of the target with a length above 0, isIllegal function call(also for an empty or undefined target). A numeric right-hand side isType mismatch. FIELD: widths are checked, and widths that add up to more than the record length ofOPEN(128 whenLENis left out) areFIELD overflow.GETandPUT: a record number that rounds to 0 isBad record number, a negative one isIllegal function call.OPEN ... LEN: above 128, the default/S:size, isIllegal function call.WAIT: the operands are checked as bytes. The interpreter has no ports, so aWAITthat would wait for ever stops withIllegal function call.
A double is taken to single precision before FRCINT rounds it, so
2.49999999# AND 3 is 3.
Done for the JavaScript backend (#43): the runtime has _frcint and
_conint, which copy to_frcint and to_conint. STRING$, SPACE$,
LEFT$, RIGHT$, MID$, CHR$, the start of INSTR, ON...GOTO and
ON...GOSUB, INP, OUT and the value of POKE use the byte check. TAB and
SPC in PRINT check 16 bits, and the argument is evaluated when its turn
comes, so PRINT "A";TAB(40000) prints the A first. PEEK and the address of
POKE accept -32768 to 65535. RANDOMIZE, WIDTH and ERROR n check as the
interpreter does. The MID$ statement is a runtime function with the same
checks as the interpreter, and it no longer deletes characters when the
replacement is shorter than the length. Every array subscript goes through
_ix: Overflow above 16 bits, Illegal function call when negative,
Subscript out of range above the bound of its dimension. Before, an
out-of-range write grew the array. The JavaScript compile path keeps one
subscript per dimension (SemanticAnalyzer(flatten_arrays=False)), because
the nested arrays of the generated code did not work with the flat index of
the C backend. An array that is not in a DIM gets as many dimensions as its
uses have subscripts. DIM A(2.5) rounds (also for the C backend), and a
DIM bound above 32767 is Overflow when the DIM runs. Test:
tests/regression/integration/test_js_range_checks.py. Done later: the
JavaScript DIM bounds are checked when the program runs (see the #60 entry
and docs/dev/JS_ARRAYS.md). Done: the file statements FIELD, GET, PUT,
OPEN ... LEN and the file numbers have the range checks, and FRCINT is
exact in both backends (see section 11 below).
Round 2 of the JavaScript backend: READ and SWAP with array elements use
the subscripts and the bounds check (before, they assigned the whole array).
OPTION BASE 1 is used by the bounds check. An array used with the wrong
number of subscripts is Subscript out of range, in JavaScript and in the
interpreter. The interpreter now checks the second and later dimensions
against their own bound (A(2,6) on DIM A(5,5)). RESUME with no error
is RESUME without error, and a handler that falls off the end reports No
RESUME in <last line>. ON ERROR GOTO 0 in a handler reports the line of
the first error. ERASE deletes the array in JavaScript: the next use makes
it again with the bounds 0 to 10, as on the binary. A fatal ERROR n in a
handler, where n has no message, prints Unprintable error. INSTR gives 0 for
an empty string and for a start beyond the end, also with an empty search
string (interpreter and JavaScript). MID$(A$,2,3)=A$ copies byte by byte over
itself, so hello gives hhhho. Done: a target shared through B$=A$
does not copy over itself on the binary (MID$(A$,2,3)=B$ gives hhelo and
leaves B$), also with A$="hel"+"lo". The interpreter and the JavaScript
compiler already do this; test: test_js_misc_edges.py.
Done for the interpreter (#43, second part). Each item was measured on the binary:
MID$(A$,2)=X$with no length is accepted. It replaces as much ofA$asX$fills, as the three-argument form does with a length of 255. A start of 0 or beyond the end isIllegal function call, a number on the right isType mismatch. The JavaScript backend does the same.CLEARalready agreed with the binary: the numbers are read as 16-bit values (CLEAR 70000andCLEAR ,70000areOverflow),CLEAR -1,CLEAR 0,0andCLEAR ,0areIllegal function call, a string isType mismatch. Tests now cover this.USRandUSR0toUSR9: the argument is evaluated (USR(1E40)printsOverflowfirst), then the call isIllegal function call, with or withoutDEF USR: there is no machine code to run. The argument can be of any type (USR("A")isIllegal function call, notType mismatch).DEF USR[n]=addressis a statement now (it was a Syntax error). The address is -32768 to 65535 and is not kept: 65536 isOverflow, a string isType mismatch.DEF USR10=1is a Syntax error, as on the binary. The lexer ends the reserved wordUSRbefore a digit, soDEF USR 4=100works and a name that starts withUSR(USRX,USR1X,USR10,USR10(1)) is a Syntax error, as on the binary. Other reserved words are not split off the front of a name.- A missing right-hand side is
Missing operand(22) when the statement ends there (end of line,:orELSE) and a Syntax error before,or), as on the binary:A=,DEF USR=,WIDTH LPRINT,MID$(A$,2)=.MID$(A,2)="X"with a numberAisType mismatch. LPOS(x)is a function now (it was an array). The argument is any type and is not used. The value is 1, the leftmost column of a printer:LPRINTis not implemented, so there is no printer column.LPOSwith no argument is a Syntax error.WIDTH 0is accepted. The width is a byte (WIDTH 256isIllegal function call,WIDTH 70000isOverflow) and 0.4 rounds to 0. With width 0 the binary prints a line break before the first character of a line and never wraps again, andTABandSPCareDivision by zero(error 11) because they divide by the width.WIDTH LPRINT nis a statement now (it was a Syntax error) and checks the byte only. Test:tests/regression/integration/test_mid_clear_usr_lpos_width0.py.
Left out on purpose, with the reason:
DIM A(32767)isSubscript out of rangeon the binary because the array does not fit in its memory. There is no memory model (seedocs/dev/NO_MEMORY_MODEL.md), so here the array is made.- The maximum number of files: with the default
/F:3the binary saysBad file numberforCLOSE #4. This interpreter allows 15 files on purpose.
Found while measuring (#61):
- Done (#61): a wrong number of arguments to a built-in (
ABS(),ABS(1,2),POS(1,2),USR(),LPOS(1,2),LEFT$(A$),INSTR(1),RND(),INKEY$()) is aSyntax error(2) when the line runs, as on the binary. The binary reads the arguments as the call runs, so the arguments up to the first unusable one run first:LEFT$(CVI("A"))isIllegal function call,LEFT$(A$,1,CVI("A"))isSyntax error. The parser marks the call (FunctionCallNode.arity_error, tableBUILTIN_ARITYinsrc/parser.py); the interpreter and the JavaScript backend raise the error. The C backend stops at compile time (ABS requires 1 argument), as it does for every run-time error. For the multi-argument built-ins the binary checks the type of an argument at the,behind it, so a wrong type isType mismatch(13) before the count is aSyntax error:LEFT$(1,2,3),STRING$("A").ABS("x",1)andLEFT$(1)staySyntax error(arity_type_checksinsrc/ast_nodes.py). A number argument is range-checked when it is read, before the count error:MID$("A",0,2,3),LEFT$("A",-1,2),STRING$(-1,2,3),INSTR(0,"A","B",4)andINPUT$(1,300,2)areIllegal function call(Overflowabove 32767), andMID$("A",0,"x",3)isIllegal function callbefore the type of"x".SPACE$(-1,2)andCHR$(300,1)staySyntax error(BUILTIN_ARG_RANGESinsrc/ast_nodes.py; interpreter and JavaScript). Test:tests/regression/integration/test_early_arg_range.py. A string given to a number argument (ABS("a"),LEN(1)) or the other way round (LEFT$(1,2)) isType mismatchin the interpreter, as on the binary (builtin_arg_kindsinsrc/js_type_check.py). - Done (#61): a FOR inside a THEN or ELSE part (
IF 1 THEN FOR I=1 TO 2:PRINT I:NEXT:PRINT "a") runs in the interpreter. The body of such a loop is the rest of the part, which has no PC, so the clause executor goes back to the statement behind the FOR (_clause_jumpinsrc/interpreter.py). This also works after a GOSUB in the body and after RESUME NEXT. A NEXT on a later line (10 IF 1 THEN FOR I=1 TO 3:PRINT I/20 NEXT) loops as on the binary: the NEXT runs the rest of the part again (_execute_next_single). Done: FOR keeps the text pointer of the NEXT that its scan found (NXTSCN), and only that NEXT can end the loop. Any other NEXT isNEXT without FOR: the second NEXT ofIF c THEN NEXT ELSE NEXT(also at the end of the loop of a FOR that is not in a clause), a NEXT that a jump reaches, and a second NEXT on a later line. The interpreter keeps that NEXT per loop variable (_for_next_node); the JavaScript compiler gives the error for every NEXT that no FOR was paired with, and the semantic analyzer no longer refuses such a program for JavaScript (runtime_next_errors). Test:tests/regression/integration/test_next_matches_first.py. - Done (#61):
INSTRgives its error from the types, not only the count (instr_shape_errorinsrc/ast_nodes.py, interpreter and JavaScript backend). A string first argument is the two-argument form:INSTR("A",1)isType mismatchandINSTR("A",1,1)isSyntax error. A number first argument is the start:INSTR(1,"A")andINSTR(1,"A",1,1)areSyntax error,INSTR(1,1)andINSTR(1,"A",1)areType mismatch. All 30 combinations of 1 and "A" with one to four arguments match the binary. - Done (#61):
WIDTH 40,1andWIDTH 30,set the width and then giveSyntax error(WidthStatementNode.bad_device).WIDTH LPRINT 40,1andWIDTH #1,40were alreadySyntax error. The C backend refuses the form. - Done (#61): a
CLEARthat succeeds forgetsON ERROR GOTO(the next error is fatal) and ends a running handler (RESUMEafter it isRESUME without error). ERR and ERL keep their values. The sameCLEARalso empties the FOR and GOSUB stacks (NEXT without FOR,RETURN without GOSUB), sends the DATA pointer back to the first item, forgets the DEF FN functions defined so far (Undefined user function) and allowsOPTION BASEagain (the base is 0). ACLEARthat fails (CLEAR ,1isOut of memory) changes nothing, and WIDTH is not changed. The JavaScript backend checks theCLEARarguments as the interpreter does (Overflow,Illegal function call,Out of memory; eachCLEARstarts from the start-up memory top and stack) and forgets the handler only when theCLEARsucceeds.CLEARends atELSEin a one-lineIF. The JavaScript and C compilers clear the variables too (see the entry onCLEARand the DEF types below). - Done (#61): when the operand that ran out is
ELSE(A=ELSE,A=1+ELSE,PRINT 1+ELSE, also afterTHEN/ELSE), the error isMissing operandandRESUME NEXTgoes on at the next line: the rest of the line is skipped,:parts and theELSEbranch too. Other missing operands (A=:PRINT 1) go on behind the:. The JavaScript backend givesMissing operand(22) for these now, notSyntax error. In JavaScript too,RESUME NEXTafterA=:orA=1*:goes on behind the:(measured again after the merge of #58 and #61). - Done (#61): an
ELSEthat noIFwaits for (a secondELSE,PRINT "a" ELSE ...,ON x GOTO 50 ELSE ...,A=1:ELSE ...) skips the rest of the line on the binary. It is not an error. Before, the interpreter and the JavaScript backend gaveSyntax error. The parser keeps such anELSEas aSyntaxErrorStatementNodewithstray_elseset, and the statement does nothing. The C backend still refuses the form. - Done (#61):
WIDTH n,xoutside anIFtakes the rest of the line,ELSEpart included, soRESUME NEXTgoes on at the next line. In aTHENclause theELSEstill belongs to theIF; in anELSEclause a secondELSEis taken too. - Done (#61):
RESUME NEXTandRESUMEafter an error in anIFline. The binary keeps the text pointer of the last statement that its main loop started (the statement behind a:, or the start of the line). It does not start the first statement of aTHENorELSEpart: theIFstarts it.RESUME NEXTscans from the start of that statement to the next:and goes on there. The binary puts a:before eachELSE. Result: - An error in the condition, or in the first statement of a
THENorELSEpart, goes on behind the first statement of theTHENpart. This holds when the error was in theELSEpart:IF 0 THEN PRINT "t":PRINT "u" ELSE X=1/0printsu. With one statement in theTHENpart,RESUME NEXTgoes on at the next line, because anELSEskips the rest of the line. - An error in a later statement of a part (behind a
:) goes on with the next statement of the part (IF 1 THEN PRINT "a":X=1/0:PRINT "b"printsb). WIDTH n,xis the exception. It fails after it ran, and the text pointer is inside it, soRESUME NEXTgoes on behind the next:also in the first statement of anELSEpart.RESUMEruns the statement again from the same place: the wholeIF, condition included, for the condition or the first statement of a part, and the later statement alone otherwise. The interpreter does this inclause_tail_after(src/ast_nodes.py),_note_clause_errorandexecute_resume. The JavaScript backend does it in_plan_statement_labels. A GOSUB or a jump in the statements thatRESUME NEXTruns works. Test:tests/regression/integration/test_resume_next_clauses.py(82 shapes measured on the binary,RESUMEandRESUME NEXT).- Done: a file number that no OPEN made.
PRINT #1,"x",PRINT #USING,WRITE #,INPUT #,LINE INPUT #,GETandPUTareBad file mode(54) for 0 to 15,Bad file number(52) for 16 to 255 andIllegal function callabove 255 or below 0.FIELD,EOF,LOCandLOFareBad file numberfor 0 to 255.CLOSE #16isBad file number(CLOSE #1of a file that is not open is no error).INPUT$(1,#1)isInput past endfor 0 to 15.EOFandINPUT$on a file open for output areBad file mode. Measured with/F:15, because this interpreter has 15 files. The JavaScript compiler gives the same errors (_file_of) and numbers its files 1 to 15 inOPEN; before, it stopped with its own text (File #1 not open). A file statement withFIELD,GETorPUTand noOPENno longer stops with_ffield is not defined. Test:tests/regression/integration/test_file_number_errors.py. - Done: the mode of
OPENcan be any string expression (OPEN M$,#1,F$,OPEN CHR$(79),...). Before, a mode that was not a literal was always input. Only the first letter counts. The operands are checked in the order of the binary: mode (a number isType mismatch, an unknown letter or""isBad file mode), file number, name (Type mismatch),File already open, the file (File not found). A record length is read for a random file only; for another mode the file is opened first and the length is aSyntax error, with the file left open.INPUT$(n,#f)past the end of the file isInput past end. The AST node hasmode_expr; the C backend stops withUnknown file modefor a mode that is not a literal. The JavaScript compiler gives the same errors and alsoFile already open,Bad file nameandFile not found(it showed a Node message before). Test:tests/regression/integration/test_open_operands.py. - Done: the JavaScript compiler makes a valid name from a BASIC name with a
dot (
A.B,FNA.B) and from a name that JavaScript or the generated program uses (MAIN,PROCESS,TRUE,ENUM,WINDOW,REQUIRE...;_mangle_var_name). Before, such a program did not run. Test:tests/regression/integration/test_js_variable_names.py. - Done:
NULLis a statement and a reserved word.NULL nchecks thatnis a byte (Illegal function call, Overflow, Type mismatch, Missing operand, as on the binary) and prints no nulls.NULL=2andX=NULLare Syntax errors. Test:tests/regression/integration/test_null_statement.py. - Done: a random file (
OPEN "R") used withPRINT #,WRITE #,INPUT #orLINE INPUT #works on the record buffer at a cursor, as on the binary, in the interpreter and the JavaScript backend. A line ends in CR LF,GETandPUTsend the cursor back to the front, and a write or a read past the end of the record isFIELD overflow. The binary starts the buffer with whatever was in that memory; here it starts with blanks. Test:tests/regression/integration/test_random_file_stream.py. - Done:
LOFandLOCcount sectors of 128 bytes, as on the binary, in the interpreter and the JavaScript backend.LOFis the size of the file at theOPEN, rounded up.LOCof an input file is the sectors read so far: 1 after theOPEN, then the position rounded up (128 bytes read is 1, 144 is 2).LOCof an output file is the sectors written out, the position rounded down. A line end counts as CR LF, two bytes, as in a CP/M file. Still different: a new file that the program closes empty hasLOF1 on the binary (CP/M keeps one sector) and 0 here. Test:tests/regression/integration/test_loc_sectors.py. - Done: a string as a condition is
Type mismatch:IF "a" THEN,IF A$ THEN,IF "" THEN ... ELSE, andWHILE "a"(the error comes before the loop is entered, so afterRESUME NEXTthe WEND isWEND without WHILE, ERR 30). A comparison of strings is a number and is fine. A WEND takes the stack entry that holds its own place (the WHILE stores the place of its WEND), so this is also true inside an outer WHILE. The JavaScript compiler keeps the open WHILE loops (_wopen) for the same reason. Test:tests/regression/integration/test_string_condition.py. - Done:
FOR I=1 TO "a"andSTEP "a"areType mismatch(13), no loop is made, and the NEXT isNEXT without FOR. In JavaScript each loop entry keeps its own NEXT (nx), so the ELSE NEXT inIF c THEN NEXT ELSE NEXTof an inner loop isNEXT without FOR, as in the interpreter. - Done: a call cut short by the end of the line, or a
MID$statement with a fourth argument, still range-checks the number arguments that it read:PRINT MID$("A",0,andMID$(A$,0,2,3)="x"areIllegal function call. The bad statement keeps the calls (partial_calls). AMID$statement cut off by the end of the line (MID$(A$,0,,MID$(A$,1,300) does the same. Test:tests/regression/integration/test_loop_and_call_error_edges.py. - Done (interpreter): a NEXT finds its FOR below a WHILE that is still open
and discards the WHILE entry, so in
FOR K=1 TO 2:WHILE K:NEXT:WENDthe WEND isWEND without WHILE(30). The FOR entry records the stack depth. The JavaScript semantic analyzer still refuses this crossed nesting. - Done:
DEFINT,DEFSNG,DEFDBLandDEFSTRare run-time statements, as on the binary (src/deftypes.py). They change a table when they run. A name without a suffix is typed from the table each time the program uses it. So aDEFINTthat aGOTOjumps over, or that is in anIFthat is not taken, does nothing. ADEFINTin a loop types the lines above it on the next pass. AfterDEFINT Athe nameAmeansA%, andA!is the single variable (A,A%andA!are three names).CLEARandRUNput all letters back to single precision.DIM A(3)andERASE Atake the type when they run. The type ofFNAis the type of the letterAwhen theDEF FNruns; a laterDEFINT AmakesFNAanother function (Undefined user function). The parser still gives each node the suffix that the DEF statements above it in the text imply.Runtime.setupmakes the nodes dynamic (a node class with atype_suffixproperty that reads the table) only when the program has a DEFtype statement, so other programs pay nothing.DEFINTtyped at theOkprompt does not reachRUN; the table that the program leaves is the table at the prompt (make_dynamicon the statement typed there). Test:tests/regression/integration/test_clear_deftypes.py(measured on the binary). The UIs that keep aProgramManagermap (Tk, curses) type a program with no DEFtype statement from that map. The JavaScript and C compilers keep the static view: the types follow the DEFtype statements in the text, so a program that runsCLEARor a DEFtype out of text order can differ there. - Done:
DEF FNis known when its statement runs, not at the start of the run:FNAbefore itsDEF FN, or after aCLEAR, isUndefined user function(18). The result has the type of the function:FNA%rounds to an integer (FNA%(3)withX*1.5is 5), a$function that gives a number and a number function that gives a string areType mismatch. The JavaScript backend does the same in a program that has aCLEAR. A compiled program cannot define oneFNname twice (semantic error). - Done:
SWAPof two variables of different types (X%andY, a string and a number) isType mismatchand swaps nothing. - Done (JavaScript):
CLEARsets every variable to 0 or "", deletes every array (a later use makes it 0..10, a laterDIMsizes it), empties the FOR and GOSUB stacks, sends the DATA pointer back, closes the files and forgets theDEF FNfunctions. It does not resetOPTION BASEor the DEF types (fixed when the program is compiled), and it does not restartRND. - Done (C):
CLEARsets every variable to 0 or "", deletes every array, empties the GOSUB stack, sends the DATA pointer back and forgetsON ERROR GOTO. The FOR loops are jumps in the C code, not a stack, soCLEARdoes not end them (NEXTafter it does not giveNEXT without FOR). It does not resetOPTION BASE, the DEF types orDEF FN. - Done (interpreter): the second variable of
SWAPmust exist already.SWAP Y,Y2withY2never used isIllegal function call(5). The first variable is made by its lookup, soSWAP Y9,Y9andSWAP Y5,Y3(first unused) are fine, and so are array elements.SWAPin the compilers is not changed. - Done (interpreter):
FOR F%=1 TO 2.6rounds the limit as an integer (1 2 3; 40000 isOverflow).DEFINT Fdoes the same. - Done (JavaScript, C): a
DIMafter aCLEARis not a compile error, asDIMafterERASEwas not. The JavaScript backend givesNEXT without FORfor aNEXTthat follows aCLEARinside its loop (a program with aCLEARchecks eachNEXT). The C backend does not (see above). - A
MID$statement whose value is the target string, as inMID$(A$,3)=A$, copies one byte at a time inside the one string on the binary (HEHEH). The interpreter and the JavaScript backend do the same. When the value is a separate string (LEFT$(A$,5),A$+"", another variable), the copy is from the old value (HEHEL), as on the binary. - The
MID$statement needsMID$(with no space.MID$ (A$,2)=X$is a Syntax error, as on the binary; the function formMID$ (A$,2,2)allows the space.CLEARwith more than three arguments is a Syntax error found before any argument runs, as on the binary.
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:
- Done (#44):
ON ERROR GOTO 0inside a live handler disables the handler and raises the pending error again, reported at the line that first failed. - Done (#45):
ON ERROR GOTO <line that does not exist>isUndefined line numberat theON ERRORstatement, checked when the statement runs. - Done (#46): a handler that ends with
ENDends the program with no error (the binary printsOk). - Done (#47): an error inside a handler is reported with its own message at
its own line, and is not trapped. It was a Python
AttributeError. - Done (#37): a handler that runs off the end of the program is
No RESUME in <last line>. - The interpreter keeps the error a handler works on in
state.pending_error.state.error_infois only a fatal error, so the UIs and the run loops no longer see a handled error as a failure. Only RESUME ends a handler. A GOTO out of a handler does not end it: the binary leaves the next error untrapped, and so do we now. - The JavaScript compiler has the same edges (it prints
in <line>since #41). AfterON ERROR GOTO 0in a handler it names the line that first failed, and a handler that runs off the end isNo RESUME in <last line>, as on the binary. One difference stays: it does not checkA$=Aat run time. The C backend hasON ERRORsince 2026-10-27, with the same edges (docs/dev/C_BACKEND.md). - Done:
ON ERROR GOSUBis aSyntax erroron the binary, at the prompt and (for a stored line) when the line runs, as for any bad statement. The interpreter and the JavaScript compiler do the same. - Done (#48): after CONT, a second ^C prints
Break in <line>and CONT works again, as on the binary. CONT arms the break handler, as RUN does. A ^C at anINPUTprompt breaks at once, and CONT asks the question again. Tests:tests/regression/ui/test_cli_break.py. - Done (#48): CONT after a run-time error runs the same statement again
(also in the middle of a line and in a FOR loop). It prints the same error
until the cause is fixed at the prompt. CONT after END resumes at the
statement after the END. After the program ran off its end, CONT prints
only
Ok. An edit after the error givesCan't continue. All measured on the binary in a pty. Only the CLI does this. A direct statement that fails after the program stopped on an error prints the bare message, with noin <line>(as the binary does). Tests:tests/regression/integration/test_cont_after_error_and_end.py. - Done (#49):
python3 mbasic FILEwith a line over 255 bytes now prints the bare message and stays at the prompt, also when the first line is the long line and the program is empty. The binary also does this. The binary RUNs a loaded file at once when it is on the command line; we wait for RUN.
3. No Ok prompt - done¶
Done (#49): the CLI prints Ok once at start-up (in place of Ready), after
every direct command, after an error message and after a program stops or
breaks. A numbered line and a blank line print no Ok, as on the binary.
Tests:
tests/regression/ui/test_cli_ok_prompt.py. The curses, Tk and web UIs have
their own status areas and did not change.
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(...). Only the CLI uses
InteractiveMode, and in the CLI print() and the I/O handler write to the
same terminal. Thus the user sees no difference, and the print() calls have
no issue. The change to self.io.output() is code cleanup.
The UI backends each carry their own copy of the command handlers - cmd_load,
cmd_merge and the rest exist four times over, once per backend,
which is why the same message had to be fixed in four places. Collapsing the
handlers onto one implementation that reports through the I/O handler is
also code cleanup.
RENUM is already collapsed: all backends call ui_helpers.renum_program().
Curses, Tk and the web UI set interactive_mode and define cmd_renum, so
RENUM in the immediate line works in all of them.
- Done (#50): the web UI sets
interactive_modewhen it starts, and definescmd_renum,cmd_delete,cmd_load,cmd_save,cmd_merge,cmd_list,cmd_run,cmd_cont,cmd_chainandcmd_system. LOAD, SAVE and MERGE use the session's in-memory files, never the server disk. CHAIN in the immediate line loads the file and runs it. CHAIN inside a running program gives an error: the web UI has no way to restart a running program with a new program (this limit is not part of #50). Tests:tests/regression/ui/test_web_immediate_commands.py. - Done (#51): Tk prints nothing after RENUM. A reference to a missing line is
still reported. Tests:
tests/regression/ui/test_tk_renum_silent.py.
5. Reserved words followed by a type suffix - done (#52)¶
NAME$, LEN%, FOR$, TO%, ERR, ERL are all syntax errors on the real
binary. Done (#52): the lexer now gives the same result. Two of the tests here
were written using NAME$ and died at that line on the binary; they have been
renamed to NM$.
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 (read_identifier in src/lexer.py) reads the identifier with
maximal munch, and looks the whole string up in KEYWORDS first. MID$ and
STR$ work because those entries are stored with the dollar. When the
whole string is not a keyword, but the string without its type character is a
reserved word, the lexer returns the keyword and leaves the type character in
the input. The RESERVED_STEMS set in src/lexer.py holds the reserved words
of the CRUNCH table. The words this implementation adds (HELP, SHOW,
SETTINGS and so on) are not in the set, so SHOW% is a plain variable. The
stray type character is a Syntax error when the statement runs (section 6).
Two traps for the rule, both handled:
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 - done (#53)¶
The binary crunches a line at entry without a message, stores it, and reports
Syntax error in 10 at RUN, when execution reaches the bad statement. The
statements before the bad one on the line run first, and RESUME NEXT goes on
with the statement behind the next :. After an untrapped error the binary
opens the line editor on the line.
Done (#53): the interpreter does the same. A statement that does not parse
becomes a SyntaxErrorStatementNode (see ProgramManager.parse_single_line
with defer_syntax_errors, and the parser and lexer flags of the same idea).
It holds the text of the bad statement, up to the next : or ELSE. Typed
lines, LOAD, MERGE, EDIT, AUTO, CHAIN and RENUM (also a bad line that
RENUM numbers again) store the line
and print nothing. LIST, SAVE and RENUM use the text of the line, so
they keep it as typed. When the statement runs it raises error 2, which
ON ERROR can trap. After an untrapped error the CLI starts the EDIT
command on the line. The EDIT prompt now has the space after the line number
that the binary prints (20 PRINT, not 20PRINT). The full-screen and window
UIs still refuse a bad line when it is typed in their editors, because they
show the error at once. They keep a bad line that LOAD or MERGE reads.
The JavaScript and C backends turn the same statement into a run-time Syntax
error (--compile-js, --compile-c). The JavaScript runtime prints
?Syntax error without the line number (#41). A line number that is too large
or missing, and an &H or &O overflow, still stop the compilation.
An unterminated string is still a Syntax error here, where the binary accepts
PRINT "abc and prints abc. This difference is older than #53.
One case is fixed. WRITE with a , or ; after the last item (WRITE 1;,
WRITE #1,2,) is accepted at entry and on LOAD, and LIST and RENUM keep its
text. When the line runs, the interpreter and the JavaScript runtime write the
items and a , with no end of line, then raise Missing operand (error 22).
The same happens in a file. WRITE #1, with no items writes an empty line.
WRITE #1 with no comma and no items is a Syntax error (error 2) when it
runs. A , or ; before the first item or twice in a row (WRITE ,1,
WRITE 1,,2, WRITE ;) is also accepted at entry and on LOAD. When the line
runs, the items before the bad separator and a , go out with no end of line
(nothing, if no item comes first), then Syntax error (error 2) occurs.
The interpreter, the JavaScript runtime and the C backend all do this. The C
backend writes the error text to the error stream and stops, as it does for
other errors without an ON ERROR handler.
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:
- Done: the JavaScript backend rounds the operands of
\and MOD to integers and checks their 16-bit range, as the interpreter does.7.6 MOD 3is 2, and40000 MOD 3isOverflow(ERR 6). - Done (GitHub issues 28 and 29,
docs/dev/C_BACKEND.md): the C backend has\and MOD (operands rounded to integers, as CINT does),/always gives a floating-point result, comparisons give -1, and AND, OR, XOR, EQV, IMP and NOT work bit by bit on 16-bit integers. - Done (#54): the C backend rounds a fractional operand of
\, MOD, AND, OR, XOR, EQV, IMP and NOT, and an operand outside -32768..32767 after rounding isOverflow.\and MOD with a zero divisor areDivision by zero.-32768 \ -1is 32768 in the C backend too: a\that can give it is typed single precision (seedocs/dev/C_BACKEND.md). - Done: in the C backend a float sum, difference, product or power that is too
large,
EXPof 87.34 or more andA^Bfrom2^126printOverflowand go on with the largest float.EXPbelow -88.72 is 0. A constant above the range is the largest float.+,-and*of integers do not wrap inPRINT,STR$, a store into a float variable, and a comparison; a store into a % variable and an operand of\giveOverflow. Not done:PRINT USING, a negativeDIMsize and a secondDIMare compile errors (docs/dev/C_BACKEND.md). - Done (#55): FOR and NEXT end a loop when the sign of (v - end) equals the
sign of the step, in the JavaScript backend (
_for_done(v, end, step)) and in the C backend. With STEP 0 the loop ends only when the variable equals the limit. Test:tests/regression/integration/test_js_for_step_zero.pyandtests/regression/integration/test_c_backend_ops.py. - Done (#56): AND, OR, XOR, EQV, IMP and NOT. The JavaScript backend matches
real MBASIC (GitHub issue 15,
docs/dev/JS_LOGICAL_OPERATORS.md), and the interpreter now does too: it rounds a fractional operand (NOT 2.7is -4) and stops withOverflowfor an operand outside -32768..32767 (40000 OR 1), whichON ERRORcan trap. - Done (#57): WHILE and WEND in the JavaScript backend are jumps, as FOR and
NEXT are (
docs/dev/JS_CONTROL_FLOW.md). A WHILE and its WEND on different lines work. Test:tests/regression/integration/test_js_while_wend.py. - Done (GitHub issue 25,
docs/dev/JS_BROWSER_YIELD.md): the browser build gives the page time to run in long loops and at INKEY; Node reads INKEY from stdin. - Done (GitHub issue 13): NEXT pairs with FOR by position in the text, the
loop-back and GOSUB return points are statements in the middle of a line, and
NEXT J,Ipasses the semantic analyzer. Seedocs/dev/JS_CONTROL_FLOW.md. - Done (#59): the JavaScript backend gives
Type mismatchat run time for a string mixed with a number, for-"a"andNOT "a", for a wrong kind in an assignment, a condition, a FOR, a built-in function, DEF FN and PRINT USING. Unary+on a string is allowed. The semantic analyzer no longer folds or rewrites such an expression. Seedocs/dev/JS_TYPE_MISMATCH.md. Done later:DIM B("a")isType mismatchat run time (seedocs/dev/JS_ARRAYS.md). Done:USR(x)is Illegal function call for any argument, as on the binary. The C backend refuses the mixed forms at compile time, on purpose. Test:tests/regression/integration/test_js_type_mismatch.py. - Done (#58): in the JavaScript backend,
RESUME NEXTgoes on at the next statement, andRESUMEruns the failing statement again, not the line. Seedocs/dev/JS_CONTROL_FLOW.md. The interpreter and the JavaScript backend now follow the binary also for an error inside aTHENorELSEpart (see the #61 entry aboutRESUME NEXTin anIFline). - Done (#60): in the C backend,
ERASEdeletes the array. The next use makes the array again with the bounds 0 to 10 (1 to 10 withOPTION BASE 1), and aDIMcan give the array a new size and a new number of subscripts. Measured on the binary: DIMof an array that exists isDuplicate Definition.ERASEof an array that does not exist isIllegal function call. This is true for an array never used, and for an array erased already. The interpreter did nothing in these cases. Now it gives the error too.- A use with another number of subscripts than the array has is
Subscript out of range. - The storage is static, so a loop of
DIMandERASEdoes not use more memory. Seedocs/dev/C_BACKEND.md. - Not done in the C backend: a second
DIMof the same array with noERASEbetween is still a compile error. The binary stops at run time withDuplicate Definition. This is a deliberate refusal. - Not done in the C backend: a
DIMafterERASEwith a size that is not a constant (for exampleDIM A(R*2)in a loop) is a compile error. The binary and the interpreter accept it. The C storage is static, so the C compiler must know the largest size. This is a limit of the design, as for everyDIMwith a variable size. - Done: the interpreter gives
Subscript out of rangeforDIM A(0)withOPTION BASE 1, also when the array exists. The binary checks the bound before it checks forDuplicate Definition. (The C backend refuses such a program at compile time.) - Done: the JavaScript backend gives
Illegal function callforERASEof an array that does not exist,Duplicate Definitionfor aDIMof an array that exists (also after a use, after a jump back, and afterCLEAR, which deletes every array),Illegal function callforDIM A(-1),Type mismatchforDIM B("a")andOverflowabove 32767. A bound can be any expression, soDIM A(N*2)in a loop withERASEworks. All were measured on the binary, withERR,ERLandRESUME NEXT, and also withOPTION BASE 1and string arrays. ADIMthat fails leaves the later arrays of its statement unmade. A plainDIMwith constant bounds at the start of the program stays a fixed array; the other arrays check their bounds at run time. Seedocs/dev/JS_ARRAYS.md. Test:tests/regression/integration/test_js_dim_erase_runtime.py. - Done:
CLEARin the C backend never fails, also when an array is not yet dimensioned (10 CLEAR 500:20 DIM A(10)is correct). - Done: the JavaScript backend keeps the number of subscripts of an erased
array at run time.
10 DIM A(3):30 ERASE A:40 DIM A(3,3):60 A(2,2)=5works, and a use with another number of subscripts isSubscript out of range. Before, the program stopped at line 20 withSubscript out of range. ADIMof an erased array makes the array with the sizes of thatDIM. Test:tests/regression/integration/test_erase_dim_rank.py. The C backend gives the same output (checked with uc80 and cpmemu).
8. serialize_expression and a unary minus - done¶
serialize_expression in src/ui/ui_helpers.py now puts brackets around the
operand of a unary minus or NOT when the operand binds looser. -(A + B) and
-(A * B) keep their brackets.
tests/regression/interpreter/test_parser_forms.py covers the unary
cases. RENUM does not call the serializer (it edits the text of each line, see
src/renum.py).
9. Statements where we are ahead of 5.21¶
Each item here is a place where a program that works here does not work on
the real thing. The two items that were here (OPEN "A" and MERGE in a
running program) are now decisions. See section 10.
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: an MBF result between 1.7E38 and the IEEE limit
(3.4E38 for single) overflows there and does not here. Only a result that is
infinite in the IEEE format of its precision is an Overflow here (section 1).
Machine infinity, 1.70141E+38, is nonetheless reproduced where it is
observable - a float divide by zero and an overflow yield it, as they do on the
binary.
Two statements accept more than 5.21 does. The user decided on 2026-10-26 to keep them as extensions:
OPEN "A"opens a sequential file for append. The file is made if it is not there. 5.21 has no append mode and answersBad file mode. The interpreter, the JavaScript backend and the C backend all accept"A".test_file_io.basusesOPEN "A", so that test is not binary-verified.MERGEin a running program merges the file and then the program continues at the next statement. On 5.21, MERGE always returns to command level. The summary line that MERGE prints at command level does not show when a running program merges, as on the binary.
11. JavaScript backend, assorted edges - done¶
Each item was measured on the binary. Test:
tests/regression/integration/test_js_misc_edges.py.
USR(x)isIllegal function callfor every argument (also a string and 70000);USR()andUSR(1,2)are Syntax errors.- A
DEF FNparameter takes the type of its letter in the JavaScript backend:DEFINT I: DEF FNQ(I)=I*2: PRINT FNQ(2.6)is 6, and 40000 is Overflow. A single parameter drops digits. The interpreter did this. FORchecks its start, end and step in that order, each at once: a string isType mismatchbefore the next expression runs, and a string loop variable with a number start isType mismatch. The failedFORleaves no loop, so theNEXTafter a trapped error isNEXT without FOR(error 1). The JavaScriptNEXTwith no loop is now that error (it went on silently), the JavaScriptFORruns start, end, step in text order, and the interpreter no longer registers a loop when a value is a string.- FRCINT (
CINT, an integer variable,\,MOD, subscripts,HEX$,OCT$, byte arguments, the logical operators): the binary adds 0.5 in the precision of the number, takes the sum to single (a tie goes to the larger magnitude), drops the fraction of the magnitude, and complements the result of a negative or zero sum. SoCINT(-2.5)is -3,CINT(-.5)is -1,CINT(.49999998#)is 1,CINT(-.49999998#)is 0,CINT(32767.49999999#)is Overflow andHEX$(2.49999999#)is 3.to_frcintinsrc/number_format.pyand_frc_intin the JavaScript runtime do this. Before, they took the double to single and then rounded half away from zero, which differs near a half. - File statements in the JavaScript backend: the file number is a byte
(Overflow, then
Illegal function callfor 256 or -1), then above 15 isBad file number. A statement on a number with no open file isBad file modefor PRINT, WRITE, INPUT, LINE INPUT, GET and PUT, andBad file numberfor EOF, LOC, LOF and FIELD.INPUT$on a number with no open file isInput past end. An output file used for input, GET, PUT or FIELD isBad file mode; INPUT from a random file isFIELD overflow.CLOSE 0does nothing,CLOSEof a closed number is fine,CLOSE 1,99closes 1 and then givesBad file number.OPEN: the number first (0 isBad file number), thenLEN(FRCINT, above 128 isIllegal function call), thenFile already open.OPEN "I"of a missing file isFile not found.GETandPUTtake the record by FRCINT: 0 isBad record number, below 0 isIllegal function call, above 32767 is Overflow.FIELDwidths are bytes and the sum must fit the record (FIELD overflow). FIELDbinds its variables to the record at once,GETfills them,LSETandRSETchange the variable and the record (before, the compiled variables were never set, soGETthenPRINT A$printed nothing).LSETof a plain string variable keeps its length. The semantic analyzer now declares variables that onlyFIELD,LSETorRSETuse.ASC("")isIllegal function callin the JavaScript backend.- The JavaScript compiler sorts the lines of the source by number (and a later line with the same number replaces the earlier one), as MBASIC does when the lines are typed. Before, a source with lines out of order ran in file order.
PRINT INPUT$(n[,#f])compiled to invalid JavaScript (anawaitin the arrow function that delays a PRINT item). The items before theINPUT$item now print first, and the item is read in the statement.OPENreads its arguments in the order of the binary: the mode (a number isType mismatch; an empty or unknown mode isBad file mode), the number,LEN, then the name (a number isType mismatch; an empty name isBad file name, also in the JavaScript backend, which crashed with the host error). The mode may be a string expression (OPEN M$,1,"F"); only its first letter counts ("Input"opens for input). The parser keeps a literal mode as before and stores any other expression inmode_expr. AnIorOfile with a fourth operand is opened (File not found comes first, and the file stays open) and then the statement isSyntax error; the length is not read. The interpreter and the JavaScript backend do this.FORwith an integer loop variable takes the start, the limit and the step through FRCINT, in that order:FOR I%=1 TO 2.5runs to 3,STEP .5isSTEP 1, and a value over 32767 is Overflow at theFOR(the variable keeps its old value). The interpreter and the JavaScript backend did not.- A
DEF FNresult takes the type of the function:FNA%rounds through FRCINT (Overflow over 32767),DEFINT K: DEF FNK(X)=X*1.5gives 5, and a string body for a number function (or the reverse) isType mismatch. LOFis the size the file had when it was opened, in sectors of 128 bytes (a new file is 0, and writing does not change the number); the backends gave the length in bytes.FIELDvariables are views of the record in the JavaScript backend: a second overlappingFIELDshows whatLSETwrote through the first, andMID$(F$,..)=changes the record. A variable that was given another string (A$="plain") is out of the record for good:LSETkeeps its length andGETdoes not fill it. The JavaScript runtime finds this by comparing the variable with the string it was shown last, soA$=A$after aGETdoes not detach it, and an assignment of the same text is not seen. A variable that noFIELD,LSETorGEThas set holds memory garbage on the binary (D$of aFIELDthat was never read); that is not copied.- The JavaScript compiler no longer stops with "Semantic analysis failed"
for a
NEXTthat matches noFOR(NEXTwith no loop,NEXT Jfor another variable,NEXT I,I); these are run time errors (NEXT without FOR) thatON ERRORcan trap.SemanticAnalyzer(lenient_next=True); the C compiler still rejects them. After such an errorRESUME NEXTgoes on at the next statement in the interpreter and in the JavaScript backend; the binary sometimes runs theNEXTa second time when the variable did not match (FOR I=1 TO 2:NEXT J:PRINT IgivesE 1,E 1and 0 there). This is not copied. - Not done: the backends allow 15 files, the binary 3 by default (
/F:nraises it;cpmemudid not pass the switch on, so the number above 3 was not measured with a bigger limit). Done since:PRINT #nandWRITE #non a random file write into the record, as on the binary (see above); a number with no open file gives the errors of the binary in the interpreter too (test_file_number_errors.py), andGETandPUTwork beforeFIELD(the record buffer is there fromOPENon);FIELDviews are done, see "round 2" below.OPEN "A"is an extension and stays. - Done (round 2): the interpreter follows the binary for FIELD views:
FIELDgives the variables their part of the record at once, a second overlappingFIELDshows whatLSETwrote,MID$(F$,..)=changes the record, a variable that was given another string is out of the record for good (GETdoes not fill it,LSETkeeps its length),LSETandRSETof a plain string variable keep its length, and the variables keep their text afterCLOSE.FIELDwith a number variable isType mismatch, after the variables before it. A new record is spaces here (garbage on the binary). - Done (round 2): the loop stack. The interpreter and the JavaScript backend
keep the
FORloops in the order they were entered, with the NEXT the scan of theFORended at (section "FOR and NEXT sites" inJS_CONTROL_FLOW.md). ANEXTat another place isNEXT without FORand spoils the loop and the loops below it. So afterFOR I=1 TO 3:FOR J=1 TO 1/0:NEXT:...:NEXT IwithRESUME NEXT, the bareNEXTis error 1 and the nextNEXT Itoo (the outer loop is gone). An Overflow in theNEXTof an integer variable spoils the loop the same way. ANEXTthat finds its loop drops the loops entered after it. A subroutine does not see the loops of its caller (NEXTin a subroutine for a loop of the caller isNEXT without FOR), andRETURNdrops the loops of the subroutine. AFORwith noNEXTisFOR Without NEXTbefore the variable is set. TheFORthat runs again drops its old loop and the inner loops as soon as the start value is in. Test:tests/regression/integration/test_for_next_sites.py. - Done (round 2): a
DEF FNis its name and its type.DEF FNA$is notFNA(Undefined user function, error 18). The type of an unsuffixed name is the DEF type of the letter afterFNat theDEFand at the call (DEF FNE(X)=X*2: DEFINT E: PRINT FNE(1.6)is error 18). A parameter without a suffix has the DEF type of its letter when the call runs, not when theDEF FNwas read:DEF FNA(X)=X*2: DEFINT X: PRINT FNA(1.6)is 4, andDEFINT X: DEF FND(X)=X*2: DEFSNG X: PRINT FND(1.6)is 3.2. The parser keeps the DEF types of the call inFunctionCallNode.def_types. FIELD NAME$(a reserved word, soNAMEthen$) is a Syntax error of the whole statement in the JavaScript backend too. It reportedType mismatchbefore.- Not done: a variable in the body of a
DEF FNthat gets its type from aDEFxxxafter theDEF FN(DEF FNE(X)=X+Y: DEFSTR Y: Y="5": PRINT FNE(1)isType mismatchon the binary, which reads the type when the call runs). The interpreter types the body variables when the line is read. The JavaScript compiler takes the DEF types of the whole program (DEFINT XafterX=1.5also makes the earlierXan integer). Both come from the design of the typed variables, not from one statement. - Done: a
FORenters itsNEXTon the binary, also when the loop runs. With another name (FOR I=1 TO 2: PRINT "a": NEXT J) the errorNEXT without FORcomes at theFOR, with the line of theNEXT, before the body runs, andIkeeps its value. AFORwhose scan ends at aNEXTof an outer loop does the same. The interpreter and the JavaScript backend do this now.RESUMEandRESUME NEXTmake ERL the current line: until a new line starts, an error on the line they go back to is reported at ERL. This also holds afterREADof a badDATAitem, where ERL is theDATAline. Test:tests/regression/integration/test_loop_and_call_error_edges.py. - Not done: a spoilt
FORentry also hides theGOSUBframes below it on the binary (the nextRETURNisRETURN without GOSUB). The loops and theGOSUBframes are separate here, so only the loops are dropped.
The cause is in NEXT (bimisc.mac): it cuts the stack back to the FOR
entry it finds before it checks the text pointer, so the GOSUB frames
above that entry go too. To copy it, the interpreter needs one stack for
FOR and GOSUB entries. That is a large change for an error path, so
it waits.
- Done (random PRINT test against the binary, 2026-10-26): a numeric
constant is double when more than 7 digits are typed from the first one
that is not 0 (1000000.0, 10000000, 123456.70); + of strings in
the JavaScript backend gives String too long above 255 characters; the
semantic analyzer no longer rewrites X AND -1, X OR 0, X XOR 0 and
X * 1# to X (the operators round to 16-bit integers, and 1# widens
the result), and its constant folder rounds the operands of the logical
operators and gives up on one out of range; VAL gives Overflow above
1.70141E+38 and 0 below 2^-129. Test:
tests/regression/integration/test_literal_precision_and_rewrites.py.
A second random test of READ and PRINT USING found more, all done:
DATA with nothing after it is one empty item; RESTORE n goes to the
first item at or after line n in the JavaScript backend too, and a line
that is not there is Undefined line number (then Out of DATA); a bad
item read into a number variable stores the number it starts with before
the Syntax error; a program line keeps its trailing spaces (an unclosed
string ends with them); PRINT USING with no field stops before the end of
the line. Test: tests/regression/integration/test_data_restore_using.py.
A third random test, of the built-in functions, found two more, done:
CVI, CVS and CVD read the first 2, 4 or 8 bytes of a longer string
(a shorter one is Illegal function call), and an argument that the
binary range-checks as it reads it gives its error before a later
argument can give Type mismatch (MID$("a",-1,"x") is error 5). Test:
tests/regression/integration/test_function_argument_edges.py.
A fourth random test, of control flow, found one more, done: a FOR with
a double-precision variable (I#, or a DEFDBL letter) is Type
mismatch after the start is worked out and before the limit is read, in
the interpreter and the JavaScript backend. Test:
tests/regression/integration/test_loop_and_call_error_edges.py.
TAB( and SPC( are PRINT items only: in any other expression they
are a Syntax error, and PRINT TAB(2)+1 is two items. The same random
test, run against the C backend, found more, done there: VAL skips
blanks inside a number, POS exists, RESTORE n goes to line n, a DATA
item +7 compiles, and READ A$ of a number item takes the text as it
was typed. A second run against the C backend found three more, done:
ON x GOTO and ON x GOSUB round the index (ON 1.5 takes the second
line), READ of a text DATA item into a number skips blanks like VAL
(DATA 1 2 is 12), and an integer FOR rounds its limit and step (STEP
.5 is STEP 1; it was 0, a loop that never ended). Tests:
test_function_argument_edges.py, test_c_backend_edges.py,
test_c_backend.py.
Still different, by design (MBF arithmetic is not emulated): 100^16
prints 9.99999E+31 on the binary, a single product between 1.70141E+38
and 3.4E+38 is Overflow there, and 32768*-32769 prints -1.07377E+09.
The random tests are utils/fuzz_real.py (interpreter, JavaScript) and
utils/fuzz_real_c.py (C backend). A fifth run, with the JavaScript
backend on control flow, found one more, done: NEXT in the interpreter
steps a single-precision variable in single precision, so
FOR I=1E10 TO 1E10 STEP 3 never ends, as on the binary (1E10+3 is 1E10).
Test: test_loop_and_call_error_edges.py. A tie (16777216+1) still rounds
to even, not up as MBF does, in NEXT as in +.
A run of 100 seeds in each mode found two more, done in the interpreter
and the JavaScript backend: AND, OR, XOR, EQV, IMP, \ and
MOD make the left operand a 16-bit integer before they read the right
one (65535 AND 1/0 is Overflow, "a" AND 1/0 is Type mismatch),
and a single gets its six printed digits by FOUT's steps of ten (see
NUMBER_FORMATTING.md; the C runtime too). Test:
test_operand_order_and_single_print.py.