The C backend¶
mbasic prog.bas --compile-c out compiles a BASIC program to C and builds a CP/M
.com file. The compiler is src/codegen_backend.py. The C runtime is in
runtime/strings/ (mb25_string.c and mb25_string.h). The default toolchain is
uc80 (see TOOLCHAIN_POLICY.md). This page tells what the
backend does and where it differs from the interpreter.
A construct that the compiler cannot compile¶
The backend never writes C that behaves differently from the interpreter without a message. There are two kinds of message:
- An error stops the compile.
--compile-cprints each error with its line number, writes no C file, and exits with status 1. - A warning gives advice.
--compile-cprints each warning and continues.WIDTH,CLEARwith memory sizes, and double precision on uc80 give warnings.
A built-in with the wrong number of arguments (ABS(1,2)) and WIDTH n, device
are errors too. The interpreter gives Syntax error for them when the line runs; the C
backend has no run-time errors, so it stops at compile time.
The backend gives an error for a statement or function that it does not have. It
also gives an error for a string where a number is needed (Type mismatch).
The error is at compile time on purpose; the JavaScript backend raises it at
run time (docs/dev/JS_TYPE_MISMATCH.md).
Before this change the backend filled self.warnings and nobody showed it. It wrote
an unsupported construct to the C file as a comment.
Examples of errors: INKEY$, INPUT$, TRON, RUN, FILES,
NAME, CHAIN options, string-valued DEF FN, and a FOR loop without a NEXT below
it. PRINT USING was an error too until 2026-10-27 (the code for it printed the text
%.*f, because the uc80 printf has no * width); see PRINT USING.
Numbers¶
PRINT writes every number through the runtime. The rules are the rules of
src/number_format.py (format_for_print):
- A number that is not negative has a space before it. Every number has a space after it.
- There is no leading zero:
.333333, not0.333333. - A single-precision value shows 6 digits. The rounding has the same two steps as the interpreter (7 digits, then 6).
- A value that is too big or too small for plain notation uses
Enotation:1E+06,1.5E-09. STR$gives the same text with a space before a number that is not negative and no space after it.WRITEgives the digits alone.
The number of digits comes from the type of the expression, not from the value.
The backend asks number_format.numeric_digits(), the function that the interpreter uses.
/ always gives a floating-point result: 5/2 is 2.5. \ and MOD round their
operands to integers. A rounded operand of \, MOD, AND, OR, XOR, EQV,
IMP or NOT outside -32768..32767 stops the program with Overflow, and \ or
MOD by 0 stops it with Division by zero. A comparison gives -1 for true.
AND, OR, XOR, EQV, IMP and NOT work bit by bit on 16-bit integers.
^ with a whole exponent uses
repeated multiplication, because the uc80 pow() is not exact (3^8 gave 6560.98).
The formatter builds the exact decimal value of the float in a digit array and then
rounds it. It needs no printf and no 64-bit integers. The code is mb25_fmt_num()
in mb25_string.c. A test compared it with format_number() for 3000 random
single-precision values, and all of them agreed.
Code size¶
The .com size of the program in issue 29 (PRINT "HI";2+3 and the next lines):
- Before: 22144 bytes, with the
--printf floatconverter linked in. - After: 23808 bytes. The number formatter and the output routines add about
4 KB. The build now passes only the
--printfand--scanfconverters that the generated C uses, which saves about 2.3 KB.
A program that uses strings also links string functions that were not reachable
before (CHR$, HEX$, INSTR and others), so these programs grow more.
Output¶
PRINT goes through mb25_print_* in the runtime, which tracks the cursor column.
So the runtime does the comma zones of 14 columns, TAB, SPC and the 80-column
wrap. After INPUT the column is 0. PRINT #n and WRITE #n send the same text to
a file, with CR LF at the end of each line. LPRINT writes to the console.
The mode of OPEN must be a string literal. The compiler stops with Unknown
file mode for OPEN M$,#1,F$, which the interpreter and the JavaScript
compiler accept.
Arrays¶
A numeric array is a flat C array. A string array is a run of consecutive descriptors in the string pool, so element n has the ID of the array plus n.
DIMdoes nothing when the program runs. The sizes must be constants (the semantic analyzer checks this). The program start sets all storage to zero.- An array that has no
DIMhas 11 elements for each subscript. OPTION BASE 1works. The program rounds every subscript to an integer and checks it. A subscript above the array stops the program withSubscript out of range in 40. A negative subscript givesIllegal function call in 40.- A string array element works wherever a string variable works: assignment,
expressions,
PRINT,INPUT,LINE INPUT,READ,SWAP,MID$assignment,LSETandRSET. ERASEdeletes the array, as on MBASIC 5.21. The next use makes the array again with the bounds 0 to 10 (1 to 10 withOPTION BASE 1), and all elements are 0 or the empty string. ADIMafterERASEcan give the array any size and any number of subscripts. An array that anERASEnames has its shape as run-time state (ea_<name>_n,_rkand_stin the C code, handled bymb25_ea_*inruntime/strings/mb25_string.c). The storage has the size of the largest shape that aDIMor a use can give, so a loop ofDIMandERASEdoes not use more memory.ERASEof an array that does not exist isIllegal function call,DIMof an array that exists isDuplicate Definition, and a use with the wrong number of subscripts isSubscript out of range. A secondDIMof an array that noERASEnames is a compile error. ADIMafterERASEmust have constant sizes (a size that depends on a variable is a compile error, as for allDIMin this backend).- A variable and an array can have the same name (
AandA(1)). The C names are different: arrays end in_arr, and a string array has its ownSTR_macro. - The limit is 16383 string descriptors in all (the IDs have 14 bits).
Loops and control flow¶
FOR and NEXT compile to labels and goto. A NEXT pairs with the nearest FOR
above it in the text, as in the JavaScript backend. NEXT J,I closes two loops.
A negative STEP counts down. Some programs jump into the middle of a loop, and
their NEXT belongs to a FOR further down the text. These programs give an error.
IF ... THEN statement ELSE line works in all mixed forms. ON GOSUB, and GOSUB
inside an IF, have return labels. RETURN without GOSUB stops the program.
Differences from the interpreter¶
These differences are known. They are not fixed.
- Double precision is 32 bits on uc80. A
#variable keeps about 7 digits, and the output usesDnotation with 7 digits. The compiler gives a warning for a program that uses double precision. - Integer arithmetic wraps in some places.
+,-and*of two integers give a single-precision number when the result does not fit 16 bits, as in MBASIC, in these places:PRINT,STR$, a store into a single or double variable, an operand of/and^, a comparison, and an operand of\,MOD,ANDand the like (whereOverflowfollows). A store into an INTEGER variable givesOverflow(A%=A%+1at 32767). In other places (a subscript, an argument of a function, aFORlimit) the integer result wraps at 16 bits. The compiler works out a constant such as200*200itself and writes 40000. The compiler gives a warning when a program does+,-or*on integers. A store into an INTEGER variable works in 32 bits when the expression is small enough (fast), and in double otherwise. TheNEXTof a loop with an INTEGER counter givesOverflowwhen it steps past 32767 or -32768. - Some float results are not exact. uc80 does not round a multiplication like
IEEE binary32.
PRINT 1/3*3=1gives 0 (false), and the interpreter gives -1.2/3*3=2and1/10*10=1have the same fault. The printed value is correct because the formatter rounds to 7 digits, but a test for equality on a computed float can go wrong. Compare with a small tolerance in a program that is to be compiled. SeeUC80_BUGS_TODO.md. - Error handling works as in MBASIC 5.21: see ON ERROR GOTO.
- Run-time errors (
Subscript out of range,Illegal function call,String too long,Out of DATA,RETURN without GOSUB,File not found,Bad file number) print a new line when the cursor is not in column 1, thenmessage in LINE, as MBASIC 5.21 does (no?in front). The C program keeps the line number inmb25_line(one store at the start of each line).SQRof a negative number,LOGof zero or less, a negative base with a fraction as exponent,ASC(""), a count or code outside 0..255 inLEFT$,RIGHT$,MID$,CHR$,SPACE$andSTRING$, and aMID$orINSTRstart outside 1..255 giveIllegal function call.CINT, a store into an INTEGER variable, a subscript, and the argument ofHEX$andOCT$giveOverflowoutside the range. A string longer than 255 characters stops the program withString too long. - A floating-point result can differ in the last digit.
basic/dev/bas_tests/asciiart.basdiffers in one character out of 2000. - Float errors that MBASIC reports and goes on from. A float division by zero,
0^-1, a float sum, difference, product or power that is too large,EXPof 87.34 or more, and a constant above the largest float printDivision by zeroorOverflowon a line of their own and go on with the largest float (1.70141E+38), as MBASIC does. (A constant above the range gives no message, as in MBASIC.)EXPof a number below -88.72 is 0, andA^BgivesOverflowfrom2^126. WithON ERROR GOTOset, these errors go to the handler, as in MBASIC. The operators\,MOD,AND,OR,XOR,EQV,IMPandNOTgiveOverfloworDivision by zeroas a run-time error (the handler, or the end of the program). - Not done:
DIMwith a negative size or a secondDIM. The compiler refuses aDIMwith a constant negative size or a secondDIMof the same array, with a message at compile time (MBASIC givesIllegal function callorDuplicate Definitionat run time). - Not done:
A^127. MBASIC prints onlyOverflow. The C backend prints the largest float afterOverflow. \and the minus sign on an integer give a float when they can give 32768.-32768 \ -1and-A%withA% = -32768are 32768 (also inSTR$), as in MBASIC, because the compiler types such an expression as single precision. A\with a constant divisor other than -1 stays an integer. A store of 32768 into an INTEGER variable isOverflow.-32768 MOD -1is 0 in both.- The float range is that of uc80: about 1.2E-38 to 1.7E38. A number in the program
below this range is wrong in the compiled program (
1E-38prints as0; a number above the range is the largest float, as in MBASIC), and the compiler warns about such a number.2^126prints with one digit less than MBASIC.INTandFIXwork on the whole float range. - INPUT reads a whole line. It does not give
?Redo from start, and with several variables it does not ask again for missing values. At the end of the input the program ends. - Bad data in
READis an error, as in MBASIC. Some items are bad for a numeric variable: a quoted item, and text that is not a number.READstores the number that the text starts with. Then it givesSyntax errorin the line of theDATA.&O8is aSyntax errorin the line of theREAD. A number too large isOverflow. A quoted item with text after it ("a"b) is aSyntax errorfor a string too. The nextREADstarts again at the first item of the statement that failed. The compiler finds these items when it compiles (_data_item_flag).VALof bad text gives 0, as in MBASIC. - The program must fit in memory. The build stops with an error when the code, the data and the stack reserve go above FD00h. A larger program overwrites CP/M when it starts.
- A tab character in the output is not expanded by the CP/M console.
- Random-access files (
FIELD,GET,PUT) do not build with uc80. STOPprintsBreak in <line>and ends the program. There is noCONT.
ON ERROR GOTO¶
Every run-time error goes through mb25_fail(code) in the runtime, with the
error code of MBASIC (src/error_codes.py). The runtime has one table of the
messages. A code without a message prints Unprintable error. Division by zero
and Overflow in a float expression go through mb25_warn(code), which prints the
message and goes on, or calls mb25_fail when ON ERROR GOTO is set.
When ON ERROR GOTO n is set and the handler is not running, mb25_fail sets ERR,
ERL and the number of the statement, and calls longjmp. main() calls setjmp
once, before the first line, and goes to the line of the handler. Because of the
longjmp, the variables, arrays, GOSUB stack and file tables of main() are
static in a program with ON ERROR GOTO n.
RESUME and RESUME NEXT must go back to a statement. When a program has one of
them, each statement gets a label p_N before it and stores N in mb25_stmt.
RESUME goes to p_N of the statement with the error, and RESUME NEXT goes to
p_(N+1), the label after it. The cost is one store and one switch case for each
statement. A program with only RESUME n has no labels.
The rules are those of MBASIC 5.21 (measured, see
tests/regression/integration/test_c_backend_on_error_using.py):
RESUME,RESUME NEXTandRESUME nsetERRto 0 and keepERL. Until the next line starts, an error is reported inERL.- After an error in the condition of an
IF,RESUME NEXTgoes on at the second statement of the THEN part. MBASIC looks for the next:. - An error in the handler stops the program, in the line of the handler.
ON ERROR GOTO 0in the handler stops it with the error of the handler, inERL. A handler that goes past the last line isNo RESUME. RESUMEwith no error isRESUME without error, and a handler can trap it.ON ERROR GOTOto a line that is not in the program isUndefined line numberwhen the statement runs.ERROR 0andERROR 256areIllegal function call.- A
FORthat fails (FOR I%=1E10) starts no loop, so afterRESUME NEXTitsNEXTisNEXT without FOR. WithON ERROR GOTO, each loop has a flag that theFORsets and the end of the loop clears. AFORwith a double variable isType mismatchin every program, as in MBASIC.
PRINT USING¶
The runtime formats each value when the program has calculated it.
mb25_using_begin takes the format. mb25_using_num and mb25_using_str take one
value each. mb25_using_end prints the rest of the format up to the next field,
and the new line. The code is a port of the _using_* functions of the
JavaScript runtime (src/codegen_js_backend.py), which are a port of
UsingFormatter in src/basic_builtins.py. A value of the wrong type for its field
is Type mismatch, after the text that came before it. A double has 7 digits
under uc80 (its doubles are 32 bits), so a field for more digits shows zeros.
Tests¶
tests/regression/integration/test_c_backend.py compiles programs with uc80, runs
them under cpmemu, and compares the output with python3 mbasic prog.bas --ui cli.
The tests skip when uc80, um80, ul80 or cpmemu is missing. The test finds cpmemu
through the environment variable MBASIC_CPMEMU (a path to the binary) or on PATH.
It finds the uc80 libraries in the way that --compile-c does (MBASIC_UC80_LIB, or
the uc80 package).
To check the formatter without a Z80 toolchain, compile mb25_string.c with a host
compiler. mb25_fmt_num() has no other dependency.
Faults in uc80¶
The backend works around several faults in uc80. See UC80_BUGS_TODO.md, the section about the C backend.
CLEAR¶
CLEAR sets the variables to 0 or "", deletes the arrays (as ERASE),
empties the GOSUB stack, sends the DATA pointer back, forgets
ON ERROR GOTO and closes the files. The FOR loops are jumps in the C code,
so CLEAR does not end them. CLEAR does not reset OPTION BASE, DEF FN or
the DEF types: the types follow the DEFINT, DEFSNG, DEFDBL and DEFSTR
statements in the text.