Route B · compiler coverage census · 3 October 2026, updated on 3 and 4 October (V7.0.00b5)

What the compiler leaves as text

Every program in the repository and on the PC3's SD card was compiled by the board's own compiler, and each statement it left as a text fallback was found, classified and weighted by whether it runs inside a loop. Headless workloads were then run under OPTION PROFILING to see which fallbacks actually execute. The result is the next set of statements for the compiler.

COM16: PicoMiteHDMIWEB V7.0.00b4 (development, SDK 2.3.1), RP2350B at 315 MHz, 6 MB PSRAM, OPTION COMPILE ON. The census is of the morning's build, V7.0.00b4 (stream format RB_VERSION 33). The updates re-measured the games, the benchmarks, walkr and the workloads during the day, last on V7.0.00b5 as released (RB_VERSION 37, the same firmware, at 315 MHz); the commits are in the summary.

1 · Summary

Summary

ProgramsRunnableStatementsCompiledIn loopsCompiled in loopsNow
Named games and other repository games916,54286.2%15,00789.3%93.0%
Benchmarks306,81877.8%4,17183.7%91.4%
Other repository programs and tests14837,15274.2%15,55180.1%≈ 85.6%
SD card programs (runnable on V7)36256,02866.9%40,14775.2%≈ 84.0%
Goldens corpus (B:/g)984,38530.4%51677.3%≈ 81.0%
walkr-robot (github.com/teuler)31,63932.5%1,24533.7%42.1%
All650122,56470.5%76,63778.8%≈ 85.8%

Statements exclude declarations (SUB/FUNCTION headers, DIM, CONST, OPTION, DATA, TYPE blocks), which run once or never. Now: compiled in loops on V7.0.00b5, censused again for the games, the benchmarks and walkr. ≈ marks a projection from the morning census: a fallback whose every item is now done counts as compiled, with the limits of what was done (dotted names only in programs without a TYPE; TEXT and the other drawing commands only with no FUNCTION call and no built-in still text in their arguments; MM.xxx, PEEK( and MATH( only in the forms compiled; VAL( only as the whole right-hand side of a numeric assignment; CHOICE( only with branches of one kind; MM.INFO( only with the compiler’s keywords; BLIT only in its FLASH, FRAMEBUFFER, READ, WRITE, CLOSE and plain forms; a LOCAL value that names a variable the same LOCAL makes, calls a FUNCTION or a built-in still text, or is an array’s list stays text, and so does a structure type). Against the three groups censused again on V7.0.00b5, the projection was 0 to 1.0 point high: games 93.0% projected and measured, benchmarks 91.8% against 91.4%, walkr 43.1% against 42.1%. 264 of the 914 programs were left out as not runnable on this firmware: unknown command / undefined SUB 219, status nocompile 33, status run 7, status load 3, line map mismatch 2.

2 · Recommendation

The next set of statements

Items 1, 3 and 4 are done, and item 2 for programs that define no TYPE, all committed on 3 October. The counts in the table are the morning census's, so for those four they show what each item has taken. The list is ordered by three things, in this order:

  1. the time the fallback took in the measured workloads (section 3);
  2. how many statements inside loops need it, and in how many programs;
  3. the effort.

Statements outside loops run once, so they are not counted towards the order.

Commands whose own work is far larger than reading their arguments are left out, even where they are common in loops: CLS, FRAMEBUFFER, PLAY, PAUSE, DRAW3D, SPRITE, TURTLE, LOAD/SAVE, OPEN/CLOSE and INPUT. Compiling them would save the text hand-over and the argument parse, which is a few microseconds against a screen clear, a frame copy or a wait. In Elite's frame, for example, FRAMEBUFFER COPY, CLS and Draw3D WRITE are busy lines because of the drawing they do.
#Statement or formIn loopsAloneProgramsGamesEffort
1Array elements as SUB and FUNCTION arguments done651368718 (114)Small to medium
2Dotted names that are not structure members: R.running, srv.pCurr() half done400259320 (0)Small (no TYPEs) to medium (with TYPEs)
3CONST names as SUB and FUNCTION arguments done109109184 (59)Small
4SUB calls written with brackets: Foo(a, b) and Foo() done2,0041,351942 (3)Small
5TEXT, CIRCLE and the other drawing commands through the value splice mostly done1,2671,2671378 (290)Small per command
6Strings through calls: string arguments, string parameters, string FUNCTION results1,2885851268 (173)Medium
7LOCAL in all its forms, and STATIC mostly done8468461638 (39)Small (comment, AS type, LENGTH) to medium (value, arrays, STATIC)
8Built-in functions: TIMER, INKEY$, MM.INFO, KEYDOWN, PEEK, PIXEL, ASIN/ACOS/ATAN2, MAX/MIN, VAL, CHOICE mostly done1,8381,5693129 (148)Small each
9MATH (vector, matrix and quaternion forms)321321501 (71)Medium
10GOTO, IF ... GOTO, IF ... THEN label, GOSUB, ON n GOTO7575412 (7)Small (GOTO, IF ... GOTO) to medium (GOSUB, ON)
11CONTINUE DO131393 (3)Small
12SELECT CASE beyond 10 values, and string SELECT CASE9981436 (16)Medium
13String array elements503272675 (23)Medium
14Structure (TYPE) members315257250 (0)Medium to large
15Whole arrays as arguments, and array copy a() = b()383142954 (54)Medium
16READ and RESTORE463463857 (112)Medium
17Single-line IF ... THEN call ELSE call8176507 (31)Small to medium
18FUNCTION calls inside SUB arguments, FOR, DO/LOOP, SELECT and the splices13742403 (45)Medium
19PRINT1,5061,5065029 (215)Medium

In loops: fallback statements that run inside a loop (a FOR/DO/WHILE body or a backward GOTO, or anywhere in a SUB or FUNCTION reached from a loop or an interrupt). Alone: of those, the ones this item would compile by itself. Games: games affected (statements in loops).

1

Array elements as SUB and FUNCTION arguments

done Committed af67bb1. An element alone goes by its address (RP_ELEM, RC_ADEL); BYVAL stays text. Exile kernel: text statements per run 55,619 → 20,170, compiled share 97.8% → 99.2%, profiled run 18.12 → 16.91 s. In the census it compiles 476 in-loop statements in 51 programs that need nothing else. Test: Bas/bench/rbelemarg.py.

651 in-loop statements in 71 programs (368 alone); games: 8. Effort: Small to medium.

Today: RBArgs refuses an argument that is an element on its own, name(i [, j]), because DefinedSubFun passes it by reference, so the whole call stays text (Stream.c, the FindSubFun(q, 1) < 0 test in RBArgs). Typical statements are a = Add8(obPat(tAddr + yy), tYoff, 0) and Bar DR, DRY(3 + i), e, 3, cYellow, cRed.

To add: Compile the element's address with RBElement(..., RC_ADEL) and pass it to RC_CALL/RC_FCALL as a pointer argument: a new RP_ flag meaning the address is on the stack, not in a bind. RBMakeParams then makes the parameter a T_PTR to the element, as DefinedSubFun does. The bounds check is the one RC_ADEL already makes.

Evidence: It is the hottest fallback in the Exile kernel: 7,061 of the 78,723 samples on its 40 busiest lines (Add8, Sub8, SevenEighths(vel(0))), and 29,030 text LETs per run. It is also in Elite's dashboard (Bar, 1,664 samples) and gfx_frames (1,970).

Bar DR, DRY(0), dSpeed \ 2, 14, cRed, cYellow  ' Elite (program + library), line 2193
Pointer DR, DRY(1), 8 + alp2 * (alp1 \ 4)  ' Elite (program + library), line 2194
Pointer DR, DRY(2), 8 + bet2 * bet1  ' Elite (program + library), line 2195
2

Dotted names that are not structure members: R.running, srv.pCurr()

half done Half done, committed 418892f: in a program that defines no TYPE, dotted names compile as the plain names they are (RBDotBlocked). That compiles only 38 in-loop statements in 9 programs. The other 221 in-loop statements that need nothing but this are in programs that define a TYPE, nearly all of them walkr's, so walkr gains nothing yet (section 5). The half with TYPEs, the per-symbol flag described below, is still to do. Test: Bas/bench/rbdotted.py. walkr's author has since renamed its dotted names that are not structure members: computing a move then took 5.2 ms compiled against 8.5 ms as text (V7.0.00b4).

400 in-loop statements in 32 programs (259 alone); games: 0. Effort: Small (no TYPEs) to medium (with TYPEs).

Today: Every name whose spelling has a dot is refused as a structure member: RBVarRefS, RBElement and RBCompileLocal test memchr(sp, '.', len). Many MMBasic programs use dots as namespaces on plain variables (Dim integer R.running, Dim Float srv.pCurr(17)). And when a program defines any TYPE, findvar does not bind a dotted symbol either (FINDVAR_DOTBLOCKS: g_structcnt > 0 and SYMC_DOT), so the text path looks it up by hashing every time.

To add: When the program defines no TYPE, findvar already binds dotted symbols, so the compiler only has to stop refusing them. When it does, a dotted name is a structure member only if the part before a dot names a TYPE instance. That can be settled once per name at PrepareProgram, where the TYPEs and their instances are known (a per-symbol flag beside SYMC_DOT), and then both findvar's binding and the compiler can treat the other dotted names as the plain names they are.

Evidence: It is the main reason walkr-robot gains under 5% (section 5): 306 of its 788 in-loop text statements need it, 216 need nothing else, and fixing it alone takes walkr's compiled share in loops from 33% to 51%. It is also the style of Petrobot, superclock and the solar_eclipse INPUT lines.

Loop Until (Abs(Val(obslat.deg$)) <= 90.0 And (obslat.min >= 0.0 And obslat.min <= 60.0) And (ob  ' solar_eclipse.bas, line 1913
Loop Until (Abs(Val(obslong.deg$)) >= 0.0 And Abs(Val(obslong.deg$)) <= 360.0) And (obslong.min   ' solar_eclipse.bas, line 1938
we.wait_for_key$ = k$  ' crash.BAS, line 4
3

CONST names as SUB and FUNCTION arguments

done Committed 1649bf0. The survey marks each CONST; a CONST argument compiles as a value (RC_LDG), and its bind carries RB_BCONST, so if the name is not a CONST at run time the call falls back. Elite's demo: text statements per run 42,533 → 33,676. Test: Bas/bench/rbconstarg.py.

109 in-loop statements in 18 programs (109 alone); games: 4. Effort: Small.

Today: A variable argument binds as a target, so that a CONST, which DefinedSubFun passes by value, goes to the fallback. The record compiles, but every call with a CONST argument falls back at run time. The static census counts such calls as compiled; the measured runs show them.

To add: The survey already reads every CONST (RBSurveyConst). An argument that is a known CONST compiles as a value (RC_LDG, bound without RC_TARGET), which is what DefinedSubFun does with it; any other name stays a by-reference bind, as now.

Evidence: Elite's Bar DR, DRY(3 + i), e, 3, cYellow, cRed, where CONST DR = 260: its calls stayed text after item 1 for this reason alone. Elite's Sfx SFX_BEEP, KillShip SLOT_STAR and Crosshair SRCX, SRCY, 8 are the same. 139 compiled SUB calls in 18 programs pass a CONST name, 109 of them in loops, in Elite, Chuckie Egg, Prince of Pico and Thrust (FUNCTION calls not counted).

4

SUB calls written with brackets: Foo(a, b) and Foo()

done Committed 1ff19e2. RBCompileCall reads the list with RBArgs(..., ')') when nothing but a comment follows the closing bracket; any other form stays text, as makeargs would read it differently. 200 bracketed calls ran 204 statements as text before and 4 now. In the census it compiles 1,459 in-loop statements in 53 programs that need nothing else, the largest gain of the four. Test: Bas/bench/rbbracket.py.

2,004 in-loop statements in 94 programs (1,351 alone); games: 2. Effort: Small.

Today: RBCompileCall returns 0 when the name is followed by ( (if (*q == '(' || !RBArgs(...))), so every bracketed call goes to DefinedSubFun.

To add: Do what DefinedSubFun does. A caller's list that starts with ( is split by makeargs with the delimiters "(," (MMBasic.c, the makeargs call in DefinedSubFun), so take the outer brackets off and call RBArgs(..., close = ')') as RBFcall already does. Foo() is a call with no arguments.

Evidence: This is the most common in-loop fallback in the whole corpus: 93 programs, about 1,350 statements that need nothing else (Petrobot sprite_item(&h53,h2-8,v2-8), the solar_eclipse variants, turtle and regex tests, draw()). The repository's games rarely write calls this way, so none of the measured workloads exercise it.

MarketScreen(-1)  ' Elite (program + library), line 5403
LoadLevel((mission - 1) MOD NLEVEL)  ' Thrust, line 715
LoadLevel((level + NLEVEL - 1) MOD NLEVEL)  ' Thrust, line 290
5

TEXT, CIRCLE and the other drawing commands through the value splice

mostly done Committed f9ffb87 for TEXT, CIRCLE, TRIANGLE, ARC and RBOX: TEXT with string values, a bare-word justification (GetJustificationArg at compile time, as the handler would run it) and a #font; CIRCLE behind OPTION LEGACY's guard (RC_GUARD, as LINE). Still text: BLIT (its in-loop uses are nearly all BLIT FLASH, a keyword form), POLYGON (arrays), TRIANGLE SAVE/RESTORE, array forms, and a FUNCTION call in the arguments. Measured: 2,000 TEXTs 424 → 411 ms and 2,000 CIRCLEs 52 → 44 ms, OFF → ON: the drawing is most of their time. Test: Bas/bench/rbdraw.py (a pixel checksum of each drawing, the same OFF, ON and SHADOW). Left as text across the corpus (projected, in loops), 4 October: 278 statements. BLIT FLASH 163 (18 programs, among them Exile and Prince of Pico); the plain BLIT 20; BLIT READ, WRITE and CLOSE #n, FRAMEBUFFER and MEMORY 28; POLYGON 20 (its arrays need the splice's array kind, as MATH does); an MM.INFO( keyword in the arguments 25; a user FUNCTION call in the arguments 18 (Elite's TEXT 30, y, ViewWord$(v) + ...). gfx_frames' TEXT, its largest fallback, is held by its string array element LLAB$(i) (item 13), not by this item. Since, in 716345a: BLIT FLASH, BLIT FRAMEBUFFER, BLIT READ, WRITE and CLOSE, and the plain BLIT compile through the command splice (cmd_blit reads symbols, so the keyword and a buffer's letter stay in the text as they are). Of the 278 above, that leaves POLYGON, the MM.INFO( keywords and FUNCTION calls in the arguments, and BLIT LOAD, MERGE, RESIZE and COMPRESSED. Test: Bas/bench/rbbig4.py.

1,267 in-loop statements in 137 programs (1,267 alone); games: 8. Effort: Small per command.

Today: RBCompileSplice takes only BOX, LINE, PIXEL and COLOUR, and only numeric values (RBEvaluate).

To add: Add TEXT, CIRCLE, TRIANGLE, ARC, RBOX and the plain BLIT form to the list, after checking that each handler reads its arguments with getargs and getint/getnumber/getstring. Let the command splice carry string values too: RBSpliceValue already returns strings for the function splice ('0'+i), so RBEvaluateS can replace RBEvaluate there. TEXT's justification may be a bare word (LT), which stays in the spliced text as its symbol; GetJustificationArg reads a symbol's spelling.

Evidence: TEXT is gfx_frames' largest fallback (9,999 of 82,490 samples on its busiest lines). In Elite, CIRCLE accounts for 2,048 text executions and 2,787 samples and TEXT for 973 and 1,731. TEXT also stays text in 82 programs and all 8 games. How much of each command's time is argument parsing rather than drawing is not measured here.

TEXT MSGX, MSGY, msgText$, "LT", 7, 1, cWhite  ' Elite (program + library), line 2160
TEXT 17, DLY(i) - 1, LLAB$(i), "RT", 7, 1, cWhite  ' Elite (program + library), line 2177
TEXT 303, DRY(i) - 1, RLAB$(i), "LT", 7, 1, cWhite  ' Elite (program + library), line 2181
CIRCLE cx, cy, r, 1, 1, cGreen, cGreen  ' Elite (program + library), line 1936
CIRCLE cx, cy, r, 1, 1, cGreen, -1  ' Elite (program + library), line 1938
CIRCLE CPX, CPY, CPR + 2, 1, 1.25, cDim, -1  ' Elite (program + library), line 2184
6

Strings through calls: string arguments, string parameters, string FUNCTION results

1,288 in-loop statements in 126 programs (585 alone); games: 8. Effort: Medium.

Today: RBParams refuses a $ or AS STRING parameter, RBArgs evaluates arguments with RBEvaluate (numbers only), and RBFcall refuses a string result.

To add: A string variable argument binds as a target and goes by reference (a T_PTR to its string). A string expression is evaluated with RBEvaluateS and copied into a new local of the parameter's length, as DefinedSubFun does. A string result comes back as the FUNCTION's local string, copied to temporary memory for RC_LDS.

Evidence: Every game except the Exile physics kernel. Elite's chart and market code (DataLine y, "Economy", EcoName$(sysEco), IF SysName$() = "LAVE") and the regex and turtle tests are the biggest users.

Message "ENERGY LOW"  ' Elite (program + library), line 2165
nm$ = UCASE$(AskText$("Find system: ", 10))  ' Elite (program + library), line 2848
DataLine y, "Distance", STR$(SysDist(homeX, homeY, sysX, sysY * 2) / 10) + " Light Years"  ' Elite (program + library), line 2939
IF SysName$() = "LAVE" THEN EXIT FOR  ' Elite (program + library), line 2315
IF SysName$() = nm$ THEN found = i  ' Elite (program + library), line 2854
nm$ = SysName$()  ' Elite (program + library), line 2890
7

LOCAL in all its forms, and STATIC

mostly done Committed 74ed383. LOCAL with a trailing comment, an AS type after a name, LENGTH n, arrays (any number of dimensions) and mixes of them; LOCAL name = value, each name made and set before the next, as cmd_dim does (RC_LOCALV, the value's code, RC_LOCALSET with SetValue's conversions and evaluate's errors); STATIC on every call after the first, once each name's global exists (RBStatic finds them all before making anything, then links each local as cmd_dim does, through its own code split out as StaticVarName and StaticLink). The tests found two cmd_dim bugs, fixed in the same commit: STATIC s$ LENGTH n = value always failed, and in a STATIC list an AS type carried over to the names after it on every call but the first. Still text: a LOCAL value that names a variable the same LOCAL makes (Local a = 1, b = a), calls a user FUNCTION, or is an array's list; a structure type; the first call's STATIC. Measured, 10,000 calls OFF → ON: a SUB opening with LOCAL i As integer, f As Float, a(3) 396 → 288 ms; walkr's timing SUB (two STATICs and a LOCAL with a value) 947 → 611 ms. Test: Bas/bench/rblocal.py (29 cases, each the same OFF, ON and SHADOW). Left as text across the corpus (projected, in loops), 4 October: 62 statements. A user FUNCTION call in a value 30 (Local i=findslot()); an array with a list of values 16; a built-in that is still text in a value (CHOICE, BOUND, VAL, MM.INFO(, MATH forms) 14; a value naming the LOCAL's own variable 1. None of the measured workloads runs a LOCAL or STATIC as text.

846 in-loop statements in 163 programs (846 alone); games: 8. Effort: Small (comment, AS type, LENGTH) to medium (value, arrays, STATIC).

Today: RBCompileLocal takes only LOCAL [AS] [type] name {, name} with nothing after the last name. A trailing comment, name AS type, LENGTH n, = value or brackets send the statement to cmd_dim, on every call. STATIC is not compiled.

To add: Ordered by yield for effort: allow a trailing comment (one test); read name AS type and LENGTH n as cmd_dim reads them; then LOCAL x = value (compile the value and store it after RBLocal makes the variable) and LOCAL arrays. STATIC only has to find its existing variable after the first call: a compiled check that it exists, with cmd_dim only the first time.

Evidence: Every in-loop statement in this group needs nothing else. solar_eclipse ran 9,564 LOCALs as text (Local i As integer, Local rsun(3), rmoon(3), Local factor = 0.25 ' comment); Elite ran its STATIC 632 times, once a frame, 523 samples. walkr-robot's author measured 100,347 text LOCALs and 100,037 text STATICs in one run: its timing and gait SUBs, called about 50,000 times each, open with Static Float dt_ms(499), Static Integer p = 0, first = 1, n = 0 and Local Float t = Timer, _t. TSCP chess opens its search with Local alpha=a, beta=b, depth=d and Local i, j, x, c, f, from, too, exitF=0 at every node, and its evaluation with Local r=0.

LOCAL kb$ LENGTH 2  ' Elite (program + library), line 1654
LOCAL INTEGER i, k, b(5)  ' Elite (program + library), line 2719
LOCAL nm$ LENGTH 10  ' Elite (program + library), line 2764
STATIC INTEGER addr = 0, fadd = 0  ' Elite (program + library), line 2169
Static count%  ' StructParamTest.bas, line 490
Static float y_move(64), x_move(64)  ' brownian_bg_demo.bas, line 168
8

Built-in functions: TIMER, INKEY$, MM.INFO, KEYDOWN, PEEK, PIXEL, ASIN/ACOS/ATAN2, MAX/MIN, VAL, CHOICE

mostly done Committed 88f85a7, through the function splice: every function with no argument (TIMER, INKEY$, POS, DATE$, TIME$, CWD$); MM.HRES, MM.VRES, MM.ERRNO and the other integer ~(letter) forms; PEEK's INT8 (BYTE), WORD, SHORT, INTEGER, FLOAT and VAR forms; MATH(RAND); ASIN, ACOS, ATAN2, CINT, PIXEL(, KEYDOWN(, MAX and MIN. TIMER, INKEY$, KEYDOWN( and MATH(RAND) give a different answer each call, so SHADOW does not check their statements (as RND). Still text: VAL( (its result is an integer or a float depending on the string, which compiled code cannot follow), MM.INFO( (many keywords, results of several types), CHOICE( (it evaluates only the branch it picks: a compiled form needs jumps, not the splice), and PEEK's VARADDR, BP/WP/SP and other forms. Measured: 10,000 PIXEL( 156 → 60 ms and 10,000 t = TIMER 59 → 23 ms, OFF → ON. Test: Bas/bench/rbfuncs.py. Left as text across the corpus (projected, in loops), 4 October: 574 statements that use one of these functions, about 270 for the function's own forms: VAL( 189 (72 programs); CHOICE( 123 (17, among them Prince of Pico); MM.INFO( keywords 74 (90 programs: MM.INFO(WRITEBUFF), MM.INFO(FLASH ADDRESS n), MM.Info(HEAP)); PEEK(BP 24, PEEK(VARADDR 11, PEEK(VAR element(i), n) 8; MM.VER, MM.ERRMSG$, MM.DEVICE$ and the other MM.xxx that are not integers about 5; other MATH( forms 2. The other 300 or so are held by another item in the same statement: dotted names with TYPEs 50 (walkr), structures 48, string array elements 42, whole arrays 35, string arguments 25, other built-ins about 40. Since, in 716345a: VAL( as the whole of LET's right-hand side (its answer converted to the target's type in the splice, as evaluate converts it for cmd_let; anywhere else its type would matter, so it stays text); CHOICE( (the condition made an int by fun_ternary's C conversion, then only the branch it picks; both branches of one type); MM.INFO( for 30 keywords whose type the compiler knows (table RBInfoKw, taken from fun_info; the text spliced as getvalue gives it). Of the forms above that leaves VAL( elsewhere, CHOICE( with branches of two types, MM.INFO('s other keywords, PEEK(BP, VARADDR and VAR with an element, and the MM.xxx strings. Statements run as text in rbbig4.py's programs, b4 -> 716345a: VAL 1,237 -> 421, CHOICE 1,246 -> 16, BLIT 111 -> 9.

1,838 in-loop statements in 312 programs (1,569 alone); games: 9. Effort: Small each.

Today: RBFunction compiles only SIN to SGN, PI and RND. RBFnSplice compiles the string functions, RGB and MAP, and only bracketed ones whose arguments are all values. A function without brackets (TIMER, INKEY$), or one with a keyword argument (PEEK(BYTE a), MM.INFO(HRES), MATH(RAND)), sends the whole statement to text.

To add: TIMER becomes an RF_TIMER like RF_RND. INKEY$ goes through the function splice with no arguments. ASIN, ACOS, ATAN2, MAX, MIN and VAL go through the splice as it stands, MAX and MIN with any number of values. Keyword forms (PEEK, MM.INFO, KEYDOWN, MATH) go through the splice with the keyword copied into the spliced text and values for the expressions. CHOICE needs care: it evaluates only the branch it picks.

Evidence: TIMER stays text in 147 programs (frame timing: tFrame = TIMER), MM.INFO/MM.xxx in 177, INKEY$ in 101. Measured: PIXEL( is julia_interp's only fallback (10,428 samples, its checksum), PEEK(BYTE ...) takes 2,135 samples in the Exile kernel, MATH(RAND) 1,011 in Elite's bench copy and ASIN 951 in solar_eclipse.

tStage = TIMER  ' Elite (program + library), line 1265
prof(5) = prof(5) + TIMER - tStage  ' Elite (program + library), line 1278
t = TIMER  ' Elite (program + library), line 1846
kb$ = INKEY$  ' Elite (program + library), line 1659
k = INKEY$ <> ""  ' Elite (program + library), line 4541
DO WHILE INKEY$ <> ""  ' Elite (program + library), line 4554
9

MATH (vector, matrix and quaternion forms)

321 in-loop statements in 50 programs (321 alone); games: 1. Effort: Medium.

Today: Not compiled. Its arguments are mostly whole arrays (MATH Q_MULT qB(), qA(), qC(), MATH SLICE sQ(), , n, qA()), which the splice cannot carry.

To add: Give the splice an array kind: T_VALUE plus a letter that carries a bound array, which findvar/getargaddress accept in place of the name. MATH and every other array-taking command (BLIT MEMORY, ARRAY SET, MEMORY COPY, the vector forms of PIXEL/LINE) can then use the splice.

Evidence: It is Elite's largest fallback: 11,713 text MATHs in 600 frames and 4,196 samples, on 5-element quaternions where reading the arguments costs more than the arithmetic. It appears in 49 programs.

MATH INSERT sQ(), , n, q()  ' Elite (program + library), line 1438
MATH Q_EULER RAD(hdg), 0, 0, qA()  ' Elite (program + library), line 1459
MATH SLICE sQ(), , s, qA()  ' Elite (program + library), line 1482
10

GOTO, IF ... GOTO, IF ... THEN label, GOSUB, ON n GOTO

75 in-loop statements in 41 programs (75 alone); games: 2. Effort: Small (GOTO, IF ... GOTO) to medium (GOSUB, ON).

Today: GOTO, GOSUB and ON are not compiled. An IF whose THEN or ELSE is a GOTO or a line number is left to cmd_if.

To add: RC_GOTO already exists (ELSE uses it). GOTO label becomes a key resolved at compile time, and IF cond GOTO label / THEN label is the compiled condition plus RC_GOTO. GOSUB pushes the return as cmd_gosub does. RETURN already compiles (RC_RETURN), yet after the benchmark's text GOSUBs its RETURN still ran as text, 30,000 times, so the pair needs looking at together.

Evidence: The Micromite benchmark (B:/benchmark.BAS) ran only 76.7% of its statements compiled: every one of its timing loops is If j < 10000 GoTo BMn (9,175 samples), and 5-7 use GOSUB 4000 (1,159). Older programs in the corpus use the same style.

GoTo dobump  ' Prince of Persia (Prince of Pi, line 891
GoTo airbump  ' Prince of Persia (Prince of Pi, line 912
GoTo ResetBall  ' Breakout, line 310
GoSub gsb  ' bench_micro.bas, line 226
GoSub NearLbl  ' bench_micro2.bas, line 485
GoSub FarLbl  ' bench_micro2.bas, line 491
11

CONTINUE DO

13 in-loop statements in 9 programs (13 alone); games: 3. Effort: Small.

Today: RBCompileExit compiles CONTINUE FOR (RC_CONTFOR) but not CONTINUE DO. In the run, each text CONTINUE DO was followed by a LOOP counted as text too.

To add: An RC_CONTDO that finds the loop's DO entry as cmd_continue does and goes to its LOOP record, the counterpart of RC_CONTFOR.

Evidence: The Exile kernel's If yy < 8 Then Continue Do ran 7,053 times, with 7,053 text LOOPs after it (1,540 samples). Only a few programs use it.

Continue Do  ' Exile physics kernel (interpre, line 461
Continue Do ' restart the track  ' Road.bas, line 46
continue do  ' daytest.BAS, line 15
12

SELECT CASE beyond 10 values, and string SELECT CASE

99 in-loop statements in 43 programs (81 alone); games: 6. Effort: Medium.

Today: The SELECT record holds every CASE test, and one record is at most RB_MAXCODE = 160 code words. Across the corpus, compiled SELECTs reach 10 CASE values and every SELECT with 11 or more stays text. A string selector is refused by RBEvaluate.

To add: Chain the tests: each CASE record compiles its own values and jumps to the next CASE on no match, while the SELECT record only evaluates the selector into a cell the CASE records read. Size then no longer depends on the number of CASEs. Alternatively, use a jump table when every value is an integer constant. String selectors use the RC_OPS comparisons IF already compiles.

Evidence: Key dispatch and state machines in 6 games: Elite's key handler (35 values), the Exile kernel's Select Case i (12, 1,056 text runs), Prince of Pico's pstep and op machines, and Chuckie Egg's string SELECT CASE cel$. Few statements, but each runs every frame, and as text it tests its CASEs one by one.

SELECT CASE b  ' Elite (program + library), line 1359
SELECT CASE k  ' Elite (program + library), line 1664
SELECT CASE n  ' Elite (program + library), line 3198
SELECT CASE cel$  ' Chuckie Egg, line 540
Select Case c  ' Prince of Persia (Prince of Pi, line 687
Select Case k  ' Prince of Persia (Prince of Pi, line 3995
13

String array elements

503 in-loop statements in 67 programs (272 alone); games: 5. Effort: Medium.

Today: RBElement refuses T_STR arrays, and RBBindK refuses a string array bind.

To add: Add an RC_LDEL/RC_ADEL for string arrays (element = base + index × (LENGTH + 1)), and an RC_STS into an element with the array's LENGTH check, as cmd_let makes it.

Evidence: 67 programs, 5 games (Prince of Pico's musName(i) tables, Elite's dashboard labels LLAB$(0) = "FS").

IF part < ltCnt(i) THEN TokPart$ = ltPart$(ltFirst(i) + part) ELSE TokPart$ = ""  ' Elite (program + library), line 3100
IF part = 0 THEN TokPart$ = tk$(tok) ELSE TokPart$ = ""  ' Elite (program + library), line 3104
PutStr dg$(k)  ' Elite (program + library), line 3193
14

Structure (TYPE) members

315 in-loop statements in 25 programs (257 alone); games: 0. Effort: Medium to large.

Today: Refused as above, and a SUB with a parameter AS a TYPE cannot be surveyed, so none of its locals compile.

To add: Bind the TYPE instance and compile a member as an offset into its data (findvar's ResolveStructMember at compile time, with the instance's type checked at bind), and read AS typename in RBUnitHeader.

Evidence: Mostly the structure tests, and walkr's inp and ST.srv(i).ID (113 of its in-loop text statements).

pa(i%).x = i% * 10  ' ArrayCopyStructTest.bas, line 37
pa(i%).y = i% * 100  ' ArrayCopyStructTest.bas, line 37
points(i).x = i * (MM.HRES / numPoints)  ' LinePlotTest.bas, line 52
15

Whole arrays as arguments, and array copy a() = b()

383 in-loop statements in 95 programs (142 alone); games: 4. Effort: Medium.

Today: RBParams refuses an a() parameter, RBArgs an a() argument, and RBLetInto a whole-array target.

To add: Bind the array (RB_BARRAY) and let RBMakeParams make the parameter a T_PTR to it, as DefinedSubFun does. Array copy can call the helper cmd_let uses.

Evidence: 94 programs (the Exile kernel's harness, NewFacing = NewShip(t, x, y, z, qA()) in Elite).

NewFacing = NewShip(t, x, y, z, qA())  ' Elite (program + library), line 1460
qC() = qA()  ' Elite (program + library), line 1541
qA() = qC()  ' Elite (program + library), line 1798
16

READ and RESTORE

463 in-loop statements in 85 programs (463 alone); games: 7. Effort: Medium.

Today: Not compiled.

To add: READ binds its targets and calls a helper that steps the DATA pointer exactly as cmd_read does, storing through the binds. RESTORE label is a position resolved at compile time.

Evidence: Data-driven loaders and tables in 83 programs and 7 games. Measured: 761 text READs in Elite's 600 frames and 738 in the Exile kernel.

READ bName$(b), bNv(b), bNf(b), bNfv(b), bNf0(b), bNv0(b)  ' Elite (program + library), line 1390
READ bCan(b), bArea(b), bBty(b), bVis(b), bEne(b), bSpd(b)  ' Elite (program + library), line 1391
READ bLas(b), bMis(b), bGun(b), bExp(b)  ' Elite (program + library), line 1392
RESTORE dat_sidewinder  ' Elite (program + library), line 1360
RESTORE dat_viper  ' Elite (program + library), line 1361
RESTORE dat_mamba  ' Elite (program + library), line 1362
17

Single-line IF ... THEN call ELSE call

81 in-loop statements in 50 programs (76 alone); games: 7. Effort: Small to medium.

Today: With an ELSE, a single-line IF compiles only if both parts are assignments, because cmd_if runs a THEN part that has an ELSE itself.

To add: Give the THEN part a record of its own when there is an ELSE (an RB_PART, as the ELSE part and the no-ELSE THEN part already have), so the IF compiles to a two-way jump whatever the parts are.

Evidence: 50 programs, 7 games: IF buying THEN BuyOne i ELSE SellOne i, If immob Then RotatingPlayer Else AngleAndFacing.

IF DEMOSCENE = 2 THEN DockInput frames ELSE DemoInput frames  ' Elite (program + library), line 1252
IF eqOwned(EQ_DOCK) THEN dockComp = 1 - dockComp ELSE Sfx SFX_BOOP  ' Elite (program + library), line 1260
IF dtSink THEN BriefBreak ELSE PutCh " "  ' Elite (program + library), line 3207
18

FUNCTION calls inside SUB arguments, FOR, DO/LOOP, SELECT and the splices

137 in-loop statements in 40 programs (42 alone); games: 3. Effort: Medium.

Today: FUNCTION calls compile only in LET, IF and INC (x.calls), because once a call has run the statement cannot fall back.

To add: Allow them wherever every other check comes before the first call: SUB arguments (the binds and the target SUB are checked first) and the splices. FOR and DO do their stack work first (RBFcall's note), so those need the call moved ahead of it.

Evidence: 38 programs, 3 games (PutName NameCap$(), DataLine y, "Government", GovName$(sysGov)). Mostly in combination with string arguments (item 4).

nm$ = UCASE$(AskText$("Find system: ", 10))  ' Elite (program + library), line 2848
DataLine y, "Distance", STR$(SysDist(homeX, homeY, sysX, sysY * 2) / 10) + " Light Years"  ' Elite (program + library), line 2939
DataLine y, "Economy", EcoName$(sysEco)  ' Elite (program + library), line 2940
19

PRINT

1,506 in-loop statements in 502 programs (1,506 alone); games: 9. Effort: Medium.

Today: Not compiled.

To add: Through the value splice with string values (item 3), keeping the ; and , separators in the spliced text.

Evidence: It is the most frequent fallback by statement count (499 programs). Most in-loop PRINTs, though, are console or file output where the output costs more than the interpreting: in solar_eclipse its 4,899 samples are the UART. It ranks low for speed.

PRINT "  "; n; " typ"; sTyp(n); " bp"; sBp(n); " obj"; sObj(n);  ' Elite (program + library), line 2618
PRINT " x"; STR$(sX(n), 0, 0); " y"; STR$(sY(n), 0, 0); " z"; STR$(sZ(n), 0, 0);  ' Elite (program + library), line 2619
PRINT " area"; bArea(sBp(n)); " ene"; sEne(n); " ai"; sAI(n);  ' Elite (program + library), line 2621
3 · Measured

What actually runs as text

The static census counts statements. These runs count executions. Each workload that ends by itself was run to END under OPTION COMPILE ON with OPTION PROFILING ON, SAMPLE, 16384:

Each workload ran in the morning on V7.0.00b4 as released, and again after the day's commits, last on V7.0.00b5 as released (4 October). On that run the Micromite benchmark was sent from the corpus's copy of B:/benchmark.BAS, the SD card being out of the board; its counts are the same as before. The statements run are the same both times; what changed is how many of them ran as text.

The board stayed at 315 MHz throughout. These runs measure counts, not speed, so the clock does not matter here.

The repository's knivd.bas and julia.bas never reach END (an endless DO and a wait for a key), so their fixed-work twins stand in for them.

WorkloadStatements runCompiled, morningCompiled nowText, morningText nowText statements by command now (top)
solar_eclipse714,38296.3%99.1%26,2566,115Let 2,888, Read 221, Print 88, Dim 7, Const 7, Input 5
knivd_fixed2,207,97199.999%99.999%2119Print 6, Dim 5, Const 2, End 1, Let 1, Option 1
julia_interp3,215,21198.7%99.999%43,24341Let 18, Print 7, For 4, Const 4, Dim 1, End 1
gfx_frames1,896,39197.4%99.1%49,34217,197Text 13,000, FRAMEBUFFER 1,004, Pixel 1,000, CLS 1,000, Array Set 1,000, Read 128
benchmark430,05376.7%76.7%100,051100,044If 70,000, GoSub 30,000, Return 30,000, Print 25, Timer 8, Let 4
jbench60690.4%92.2%5847Print 17, Timer 11, Dim 3, Option 2, End 1, Const 1
elite967,00195.6%97.7%42,53322,132Math 11,713, Let 1,010, Draw3D 796, Array Set 785, Read 761, Pixel 710
exile_phys2,555,84797.8%99.4%55,61915,931Continue 7,053, Loop 7,053, If 2,537, Select Case 1,056, Read 738, Restore 18
pop2,924,118-96.7%-97,099If 28,799, Let 24,849, Select Case 10,292, Line Input 1,816, Loop 1,816, Print 1,094
chess60,438,074-99.954%-28,041Let 26,664, Read 1,064, Sprite 104, Dim 44, Const 43, Sub 21

Statements run = records the stream executed (MM.INFO(COMPILE) RAN); compiled = CODE. The text column of OPTION PROFILING counts only fallbacks by command; a fallback SUB call is not a command and shows only in the total.

The busiest lines that are text, now

From each run's 40 busiest lines ([PCSLINE]) on V7.0.00b5, the ones holding a fallback statement. Samples are 97 µs apart.

Exile physics kernel (2,384 ticks): 163,820 samples in all

SamplesLineStatementItem
1,375547If yy < 8 Then Continue DoCONTINUE DO
796877If immob Then RotatingPlayer Else AngleAndFacingSingle-line IF ... THEN x ELSE y with a call/command part

Elite, DEMOSCENE 1, 600 frames: 73,780 samples in all

SamplesLineStatementItem
1,252732Draw3D WRITE sObj(n), tx, ty, tz, 0, solidModeCommand DRAW3D
1,102701CLSCommand CLS
845123FRAMEBUFFER COPY F, NCommand FRAMEBUFFER
738797MATH Q_VECTOR 0, 0, 1, qB() : MATH Q_ROTATE qA(), qB(), qV()Command MATH
623638MATH SLICE sQ(), , n, qA()Command MATH
6191031MEMORY COPY INTEGER addr, fadd, DASHWORDSCommand MEMORY
557639MATH Q_MULT qP(), qA(), qC()Command MATH
550611MATH Q_CREATE -aT, 0, 0, 1, qA()Command MATH

gfx_frames: 139,180 samples in all

SamplesLineStatementItem
3,786367Text 17, DLY(i) - 1, LLAB$(i), "RT", 7, 1, cWhiteCommand TEXT
3,438371Text 303, DRY(i) - 1, RLAB$(i), "LT", 7, 1, cWhiteCommand TEXT
1,76890ClsCommand CLS
1,445105If FBUF% Then Framebuffer Copy F, NCommand FRAMEBUFFER
824279Pixel spx(), spy(), spc()BOX/LINE/PIXEL vector forms (array arguments), LINE PLOT/GRAPH/AA

julia_interp: 52,611 samples in all

SamplesLineStatementItem

solar_eclipse: 88,340 samples in all

SamplesLineStatementItem
9631936Input obslong.deg$, obslong.min, obslong.secCommand INPUT
9631884Input month, day, yearCommand INPUT
8261911Input obslat.deg$, obslat.min, obslat.secCommand INPUT
825223Input ndaysCommand INPUT
8251959Input obsaltCommand INPUT
426750Print " "Command PRINT
414724Print "topocentric coordinates"Command PRINT
413712Print "UTC julian day ", Str$(jdutc, 0, 8)Command PRINT

Micromite benchmark: 29,127 samples in all

SamplesLineStatementItem
10,3092Pause 1000Command PAUSE
1,36196If j < 10000 GoTo BM8GOTO, IF ... GOTO, THEN linenumber
1,35167If j < 10000 GoTo BM6GOTO, IF ... GOTO, THEN linenumber
1,33840If j < 10000 GoTo BM4GOTO, IF ... GOTO, THEN linenumber
1,33629If j < 10000 GoTo BM3GOTO, IF ... GOTO, THEN linenumber
1,30883If j < 10000 GoTo BM7GOTO, IF ... GOTO, THEN linenumber
1,30418If j < 10000 GoTo BM2GOTO, IF ... GOTO, THEN linenumber
1,25052If j < 10000 GoTo BM5GOTO, IF ... GOTO, THEN linenumber

TSCP chess, its bench for 3 minutes (the INPUT is the program waiting at its prompt for bench and bye): 1,936,620 samples in all

SamplesLineStatementItem
65,8791086Input "",s$Command INPUT

Prince of Pico, its scenario harness: 274 checks, 2,690 frames (the OPEN, LINE INPUT and VAL lines are the harness reading its scenarios): 329,420 samples in all

SamplesLineStatementItem
3,006691Select Case cString SELECT CASE
2,7644219ApplyCode Mid$(codes, i, 1)SUB/FUNCTION string arguments and parameters
2,561691Select Case cString SELECT CASE
2,2314158Line Input #3, lnCommand LINE INPUT
1,9163974Open home + "levels.dat" For Input As #1Command OPEN
1,8051141LoadChar kRec() : SetOp gRec()Whole arrays passed to SUBs/FUNCTIONs, array copy
1,7282361Select Case opSELECT CASE with more than ~10 values
1,5634160w = UCase$(ScWord$(ln, 1))FUNCTION calls inside SUB/FOR/DO/SELECT/BOX... arguments; SUB/FUNCTION string arguments and parameters; String FUNCTION results
4 · The named games

Elite, Chuckie Egg, Exile, Prince of Persia, TSCP chess

ProgramStatementsCompiledIn loopsCompiled in loopsNow
Elite (program + library)4,87885.7%4,26088.0%91.5%
Chuckie Egg1,04687.9%98790.0%93.5%
Exile (program + CSUB library)50573.3%39881.9%90.7%
Exile physics kernel (interpreted)1,01588.8%95793.2%97.4%
Prince of Persia (Prince of Pico engine)4,79390.4%4,44093.5%95.4%
Thrust1,00083.8%94385.9%91.3%
Picanoid1,53683.0%1,43686.1%91.5%
Arkanoid1,50883.0%1,42085.8%91.4%
Breakout26171.3%16680.7%84.3%
TSCP chess (Bas/chess/chessiter.bas)1,207-1,154-92.0%

Fallbacks in loops, the games only, now

ItemIn loopsAloneGames
Command PRINT21621610
SUB/FUNCTION string arguments and parameters1991438
Command READ83838
Command FRAMEBUFFER71719
Command MATH71711
String FUNCTION results68156
Command RESTORE47478
Whole arrays passed to SUBs/FUNCTIONs, array copy46433
FUNCTION calls inside SUB/FOR/DO/SELECT/BOX... arguments46154
Command CLS34349
Single-line IF ... THEN x ELSE y with a call/command part31307
Command PLAY29297
String array elements23115
Command SAVE20207
Command TILEMAP20204
Command SPRITE19193
Array elements passed to SUBs/FUNCTIONs (BYREF element)1803
Command INPUT17174
Command MAP(16162
Command TEXT15154
Command PAUSE15156
SELECT CASE with more than ~10 values11116
Command ON ERROR11119
Command DRAW3D881
Command CLOSE886
Built-in function DEVICE(882
Command OPEN776
GOTO, IF ... GOTO, THEN linenumber772
Built-in function TILEMAP(661
String SELECT CASE552
5 · A user's program

walkr-robot: why compiling gained under 5%

The code is github.com/teuler/walkr-robot, code-mmbasic, a hexapod's controller:

The servos are ST3215s on COM1 at 500 kbaud. Each program was censused as the others were, with its libraries merged in. The tester reports less than 5% from compiling.

ProgramIn loopsCompiled in loopsCorpus, for comparison
hpx_srv.bas + both libraries1,17733.1%78.8% across the 650 runnable programs, 89% for the games
configure_servo.bas5747.4%

Two thirds of the server's statements inside loops run as text, so a gain of a few percent is what the compiler can give it today. The reasons, for its 788 in-loop text statements:

NeedsStatementsNeeding only this
Dotted names that are not structure members (ggn.state, srv.pCurr(), R.running): item 2306216
PRINT (its debug and terminal output)114114
Structure members (inp.cmd, ST.srv(i).ID): item 1411362
LOCAL forms (Local Float t = Timer, Local Float xyz1(2)): item 74848
MATH on arrays: item 94444
MAX, MIN, CHOICE, ATAN2, ACOS, TIMER, PEEK: item 812542
Whole arrays as arguments, strings through calls: items 15 and 65911

The computing it times is almost all text:

The arithmetic itself compiles. Its statements stay text because they read and write the gait generator's state through dotted names, for example xyz1(0) = -ggn.xyzLeg(iL,0) + in.xyzBody_v(0) + g.xyz(iL,0) and ggn.state = GGN_MOVING. Only R._bodyIK (13 of 18) and R._legIK (14 of 22) are mostly compiled.

WithCompiled in loops
Today33.1%
Dotted names alone51.4%
The next set without dotted names or structures58.4%
The next set with dotted names83.1%
And structure members92.6%

update The dotted-name fix committed today (418892f) compiles dotted names only in programs that define no TYPE. hpx_srv.bas and configure_servo.bas both define TYPEs, so on 3071436 the server's census was unchanged at 33.1% of its in-loop statements, on 88f85a7, with items 5 and 8, it was 36.3%, on 74ed383, with item 7, 40.1%, and on V7.0.00b5 40.6% (VAL, CHOICE, MM.INFO and BLIT). The 51.4% row above needs the other half of item 2, which tells a structure member from a namespace name when TYPEs exist. Across the whole corpus, 221 in-loop statements need only that half, nearly all of them walkr's.

measured The author has since renamed every dotted name that is not a structure member (the TYPEs keep theirs), and measured on V7.0.00b4:

TimingCOMPILE OFFCOMPILE ON
Loop0.330 ms0.299 ms
Move (computing it)8.538 ms5.227 ms
Exec (servo packets on COM1)9.326 ms8.593 ms

Still, 959,055 of the 2,265,470 statements it ran (42%) were text. By command:

Three things would hold the measured gain down even with full coverage. walkr was not run here, because it needs its servos, so these come from its source:

  • Its loop is paced. R.spinGaitGen waits for ggn.tWaitUntil, so faster code makes the loop spin more often, not finish sooner. Only its Move and Exec timings (R._computeMove, R._executeMove) measure interpreting.
  • Exec includes the servo packets on COM1 and Pause in R.waitForServos.
  • The program turns Option Profiling On, which counts every statement in both modes.
6 · Census

Every item, all runnable programs

ItemIn loopsAloneInner loopsAllProgramsGames
SUB calls written with brackets: Foo(a, b)200413515452681942
Command PRINT1506150616675865029
SUB/FUNCTION string arguments and parameters113445916525901207
Command TURTLE9249243051172160
Command TEXT8558552541075828
LOCAL forms: LENGTH, arrays, AS after the name, with a value, trailing comment741741010601518
Array elements passed to SUBs/FUNCTIONs (BYREF element)651368233714718
Built-in function TIMER5114751279931508
String array elements5032722101047675
Command PLAY41041089640747
Dotted names that are not structure members (R.running, srv.pCurr())40025963706320
Whole arrays passed to SUBs/FUNCTIONs, array copy38314275710954
Command FRAMEBUFFER379379103750898
Command CLS355355446061928
Command PAUSE3493491235951676
Built-in function MM.INFO / MM.xxx3453011257171799
Command SPRITE326326169538412
Command MATH32132159565501
Structures (TYPE members, structure parameters)31525782706250
Command READ31331399424857
String FUNCTION results2489881267375
Built-in function INKEY$212202782311026
Command BLIT21121196270334
Built-in function VAL(18910842273722
Command TIMER182182193201240
Command MID$(17917986185130
Built-in function PEEK(16111779229515
Command RESTORE1501500383487
Built-in function MAX(1397952151211
FUNCTION calls inside SUB/FOR/DO/SELECT/BOX... arguments1374250163403
Command INPUT13013015180483
Built-in function SPRITE(12511474132201
Built-in function CHOICE(1238733126171
Command CIRCLE11211230154512
SUBs with more than 64 locals (RB_MAXULOCAL)107107010710
STATIC in a SUB1051050124381
Built-in function INPUT$(91769137260
Built-in function MIN(914231105220
Single-line IF ... THEN x ELSE y with a call/command part817618158507
Command BLIT MEMORY7777647790
Command DRAW3D757537110131
Built-in function KEYDOWN(74724479217
Command TILEMAP686813178184
Command ON ERROR626213411788
GOTO, IF ... GOTO, THEN linenumber626219207372
String SELECT CASE61431161322
Built-in function FRAME(6161013220
Unexplained (IF)57573364160
String literals over 64 chars / with backslash543114180182
Optional (missing) arguments54373485220
Built-in function LINSTR(530215790
Command POKE53532874191
Command MEMORY515117109444
Command CTRLVAL(484809580
Command LINE INPUT4747475323
ELSE followed by two or more spaces (compiler bug)47474351120
Unexplained (LET)4747995290
Command I2C464637790
Command SAVE45451574377
Command WEB45458127150
Command LONGSTRING454511116220
^ (power) on integers45332847130
Command END4444144452979
Built-in function TILEMAP(4141167771
Built-in function TOUCH(4036204060
Command PIN(3939267280
SELECT CASE with more than ~10 values38381360286
Built-in function EOF(3838363362
Command CLOSE36360234745
Command MODE323282141627
Command TRIANGLE32321955140
Built-in function PIN(31211160160
Command OPEN30300245865
Built-in function EPOCH(302013130
Command LOAD2828773403
LOOP of the 33rd and later DO (RB_MAXDO 32)272702710
Command FONT27271189390
Built-in function FIELD$(2616126121
Built-in function CINT(2622531141
Command ARC262682970
Built-in function LLEN(2601331110
Command SETPIN25250238591
Built-in function ASIN(242402680
Other: LINE: OPTION LEGACY (x,y)-(x,y) syntax232364530
Command POLYGON20201330141
Built-in function PIXEL(2018828100
BOX/LINE/PIXEL vector forms (array arguments), LINE PLOT/GRAPH/AA19191330171
Built-in function DEVICE(1919748162
Built-in function LOC(1812101960
Built-in function TIME$184425120
Command CAT18181020110
Command MAP(171716872
Command RAY171792510
Other: LET: other syntax1717241270
Command VAR151542260
Command PIO14140199110
Command PULSE141442390
Command ARRAY SET131331771
CONTINUE DO131361393
Command GUI13130208120
Command FRAME1212035220
Built-in function DIR$(1111029173
Command RBOX1111322111
CSUB calls1111823201
GOSUB / RETURN to a label11111129120
Built-in function JSON$(11002540
Other: ELSE: ELSE: IF table entry not found111181380
Built-in function DATE$11301670
Built-in function LGETSTR$(11061370
Built-in function DATETIME$(11401220
Built-in function ATAN2(101021041
Command I2C2101003460
Built-in function CTRLVAL(9901990
Command WATCHDOG9911440
Built-in function ACOS(870980
Command SEEK88019110
Command BEZIER885850
Command TILE8841520
Built-in function CALL(8802090
Command WS28128801320
Command ERROR77037254
Command CALL7726480
Built-in function BYTE(727770
Built-in function CWD$770770
Other: SUBCALL: unknown command or undefined SUB770810
Numeric literals over 15 digits660660
Built-in function EVAL(600610
Built-in function BOUND(5001170
Built-in function MSGBOX(550550
Built-in function STR2BIN(5552340
Command DATE$551620
Command RUN44013110
Command PWM4401350
Command SYNC442420
Command CPU440650
Built-in function CLICK(4401330
Command RTC4421230
Command MAP33137154
Unexplained (LOOP)331331
IF/ELSEIF chains too big for one record330331
Command ERASE33017120
Command EXECUTE330750
Command FILL331430
Command SORT33013100
Built-in function TEMPR(300630
Command TEMPR START3301240
Command FLASH22050325
Other: LET: FUNCTION without brackets210221
Built-in function PORT(220211
Unexplained (BOX)220220
Unexplained (ELSE IF)222210
Command SETTICK22061230
Built-in function DAY$(200220
ON n GOTO/GOSUB220330
Built-in function GETSCANLINE212220
Command ON PS2220420
Command PORT(220530
Command TIME$221310
Built-in function MATH(20020540
Command I2CLCD110710
Command INTERRUPT110520
Command FLAGS110420
Command STEPPER1107910
Command ONEWIRE1103520
Built-in function LGETBYTE(100110
Command ADC110210
Unexplained (PIXEL)111440
Command BIT(110510
Built-in function PIO(10041110
Command LIST110640
Other: IF: FUNCTION without brackets110110
Unexplained (SUBCALL)111220
Built-in function LOF(110330
Command COPY110110
Unexplained (SELECT CASE)110110
Unexplained (FOR)1101160

Inner loops: nesting depth two or more, counting a hot SUB as one level. All: every fallback statement needing the item, in a loop or not.

7 · Limits and findings

Limits, four bugs, a cost, and deep recursion

Fixed limits in the compiler

LimitEffectSeen inChange
RB_MAXCODE 160 words per recordA SELECT CASE of 11 or more values, and a long IF/ELSEIF chain, never compile: every arm's test lives in the one record.SELECT: 42 programs, 6 games (item 10). ELSEIF chains: 11 IFs in 7 programs, among them Prince of Pico's StepCharacter, zmim and chess.Chain the tests through the CASE/ELSEIF records (item 10).
RB_MAXDO 32The DO table holds the first 32 DOs of a compile, so the LOOP of every later DO runs as text: once per iteration.27 in-loop LOOPs in one SD program. The games stay under 32.Size the table from the survey's DO count, in the compiler's temporary memory.
RB_MAXULOCAL 64A SUB or FUNCTION with more than 64 parameters, LOCALs, STATICs and CONSTs cannot be listed, so none of its locals compile.B:/planets.BAS coordinates (93 locals, 107 in-loop statements).The same: from the survey, in temporary memory.
A header the survey cannot readA parameter AS a structure type makes RBUnitHeader give up, and every local in that unit stays text.The structure tests.Read the structure name as a type word.
Numeric literalsRBLiteral takes at most 15 digits, so 90.11969000000001 sends its statement to text.solar_eclipse (350 samples), planets.BAS.Read 16-17 digits; getvalue already does.

A bug, now fixed: IF and ELSE with two spaces after the command

The tokeniser skips one space after a command and keeps the rest; skipspace then steps over them, so cmdline - sizeof(CommandToken) is the command token only when there were none. Three places looked the IF table up there:

In the corpus, all 65 ELSEs with two or more spaces before a comment were fallbacks, 51 of them in Petrobot, and all 95 with one space compiled.

fixed and committed on development, 3 October (6497832). CmdTokenBefore() in Commands.c walks back over the spaces (a token's bytes are never spaces), and RBCompileExit is given the token. Measured with Bas/bench/elsespace.py, 2,000 passes over a 40-line block:

CaseOne spaceThree spaces, beforeThree spaces, after
ELSE, compile OFF34 ms84 ms35 ms
IF, compile OFF33 ms78 ms34 ms
ELSE, compile ON8 ms51 ms6 ms

The census of the six programs with such ELSEs now compiles every one of them, and nothing else changed.

A cost, now removed: comment and empty lines inside compiled loops

A tester ran a compiled program 20,000 times a second as text and 37,000 compiled, and then 57,000 after MMEdit's crunch, which takes out comments, blank lines and indentation. The cause was the stream, not the interpreter:

Measured on a two-statement compiled loop, 100,000 passes: no comment 237 ms, one comment line +18%, three comment lines +30%.

fixed and committed on development, 3 October (3071436, RB_VERSION 35). Such a line now has no record, only a map entry pointing at the next statement's record:

Two-statement loop, compiled, 100,000 passesBeforeAfter
No comment237 ms237 ms
One comment line+18%236 ms
One empty line+18%237 ms
Three comment lines+30%236 ms

solar_eclipse.bas with its five reference inputs, compiled, took 7.27 s as written and 6.93 s crunched (LOAD "...",C) before the fix, and takes 6.81 s as written after it. Crunching has little left to save: a compiled statement never reads its text, so indentation costs only the statements that still run as text.

fixed too, c1cddda: a /* ... */ block. tokenise keeps the lines between the /* and the */ as plain text, and cmd_comment steps over them, but the /* had a fallback record. walkr-robot's main loop has three blocks, so it ran 149,461 text /* statements in one measured run. Every statement from a /* to its */ now has a map entry and no record, as a comment line has, and the compiler's surveys step over the block. A block in a compiled loop now costs nothing: 245 ms against 246 ms for 100,000 passes (Bas/bench/rbblockcmt.py).

One visible change: TRACE in a compiled run no longer lists comment, empty or label-only lines. Tests: Bas/bench/rbnopchain.py (OFF, ON and SHADOW print the same, errors and MM.ERRLINE after a comment line included, and the timing) and Bas/bench/rbstamp.py.

A fault found on the way: the end of a saved program

The test for item 8 faulted with OPTION COMPILE OFF, so in the interpreter itself, for one program length in 256. The program's last line was a false single-line IF with no ELSE:

fixed and committed on development, 3 October (bc0c157), in the writers, not the runtime: SaveProgramToFlash, SaveProgramToRAM, LIBRARY LOAD's writer and LIBRARY SAVE now put a word of zeros after the marker. Every reader already skips any run of zeros before the FF, so the symbol table and the CSUB and font records are found where they are written:

An image saved before the fix keeps its old ending until it is saved again.

A third bug: NEXT in a recursive SUB or FUNCTION

Found with TSCP chess (B:/basicchess/chessfaster.bas), whose search recurses with a FOR loop around each call. The fault was in the interpreter as well as in compiled code.

fixed in 5b90865: cmd_next and the compiled NEXT close another loop only at their own level. Bas/bench/fornextrec.py fails on the build before and passes after. main has the same code.

Deep recursion: the C stack and the local variables

With NEXT fixed, the chess program's depth-5 bench met two limits.

The C stack. On HDMIWEB it runs from 0x20082000 down to heapend: 8 KB of scratch RAM, plus 4.2 KB below it, about 12.4 KB in all.

The local variables. With enough stack, chessfaster's full depth-5 search reached ply 14 and 21 call levels, using 13.9 KB of stack. Then it stopped with "Not enough Local variable memory".

How much stack each call costs, and how many locals a build allows, are left for a later decision.

since Peter's Bas/chess/chessiter.bas (eaeab32) runs the same search as a loop, with each ply's alpha, beta, depth and move index in arrays, so a node no longer costs a BASIC call: the deepest call path is 7 levels at any ply. It is the version measured in sections 3 and 4. The original's recursion inside FOR loops is the form the manual documents as unreliable.

A fourth bug: Prince of Pico did not run on HDMIWEB V7.0.00b4

fixed in db2353e (4 October). The scenario harness (A:/gb/gbpop.bas, loaded with ,C) stopped at line 117, Dim INTEGER vPanelB, ..., with Error : Not enough memory for 2048 bytes: put OPTION SYMBOLS OFF first, compiled or not, after a CPU RESTART, without its profiling twin, and with OPTION SYMBOLS OFF too (the hint was wrong). MEMORY showed 192 variables in 197 KB and 6 MB of PSRAM free.

The cause was not the compiler. Since 509622a (1 October) the globals' records are in the heap, in chunks of 32 records (2 KB on the RP2350), taken from the SRAM heap only, as each chunk filled. The heap puts an array in SRAM first, so the game's arrays had taken all of it, and its 193rd variable could not get a seventh chunk. The locals' chunks, taken the first time the local stack is that deep, could fail the same way in the middle of a run.

Test: Bas/bench/varpsram.py, which fails on the build before with the same error. The harness now passes its 274 checks compiled and is measured in section 3: 96.6% of what it runs is compiled.

The same work found the RP2040 PICO image 4 bytes over its flash limit since 74ed383 (item 7 split two helpers out of cmd_dim for Route B). d53c5df inlines them back on the RP2040, where Route B does not exist: every RP2040 variant fits, PICO with 156 bytes to spare.

8 · Method

How the census was taken