How MMBasic spends its time, what has already been learned, and a measured programme to make the interpreter faster without shrinking the largest program a user can run. It is led by one benchmark, solar_eclipse.bas, on both boards.
A read-only review of the development branch. Nothing was edited, built or run on a board to write it. Every figure is one of three things: Peter's measurement, a reading of the code cited as file:line, or an estimate marked as such.
On 26 September Peter paused the programme to bring what it has learned to main, in this order:
docs/Main_Bug_Report_2026-09.html.Route B resumes afterwards, from its design review.
| Step | Commits | Result |
|---|---|---|
| Phase 0, Phase 1 | 0183e77 … a166789 | docs/Interpreter_Phase0_Results.html, docs/Interpreter_Phase1_Results.html |
| S10 allocator hint | b4ef437 | top-down first-free hint only |
| S1c interrupt-pending counter | 791bf93, 6c6fdd8, 862ec45, 142734c | IntReady counter signalled by the sources; InterruptUsed removed; tests in Bas/bench/inttests |
| S1a ProcessWeb cadence | a07e8f2 | no network work without a connection; WEB SCAN polls; anchor 1.2-1.4% faster on HDMIWEB |
| Trace cache removed (S5/S6 phase 1a) | 04361c0 | DO fast path kept on all builds; frame tracking and per-SUB profiling under SUBPROFILE |
| S5/S6 symbols and bindings | e3298b9, 9cdcfe8, b44dacc, 317da5b | design in docs/Interpreter_S5S6_Symbols_Design.html. Symbols against text, same firmware: PC3 anchor -8.2%, Exile -5.7%, Elite -9.2%, Prince of Pico -11.4%; RP2040 anchor -12.4%, julia -19.1%, knivd -7.3%. |
| Two fixes found while designing Route B | 8d3ee25, 8740953 | CHAIN and SAVE/LOAD CONTEXT rebuild the symbol bindings (Bas/bench/symchain.py); the comment fast path no longer leaves a loop whose first line ends in : ' comment (Bas/bench/cmtloop.py, also on main as bug 21) |
Bas/bench/g3; results: Bas/bench/results/g3.docs/Interpreter_RouteB_Design.html (9650f69). Five decisions await Peter's review, and P1 does not start before them..heap runs 1,660 B past RAM with symbols, even after the symbol helpers moved to flash on that build (317da5b). The options put to Peter:
cmd_loop and cmd_print to flash on WEB RP2040 only (recommended).findvar).MMBasic spends most of its time interpreting, not computing. The work it repeats on every execution is fixed for the whole RUN. This plan measures that work first. It then moves the answers into the stored program, written once when the program is saved, and never into heap tables that grow with the program.
solar_eclipse.bas about 80% of the run time is interpreter overhead: 1 − 2.5/13 = 80.8% from Peter's figures, or 1 − 2.7/13 = 79% from the 252 MHz pair in docs/mmb2csub.md:670.OPTION PROFILING, extended one argument at a time as the hypotheses need it.Tuning hot functions has been tried several times and has not moved the overhead:
Integer and float benchmarks run at almost the same speed, so the arithmetic is not the cost. The question is not "which function is slow". It is "what does the interpreter work out again on every execution that it could have worked out once".
development branch, and everything may change: the stored program format, the tokeniser, LIST/EDIT/SAVE, the library and RAM-slot image formats, and interrupt-check timing. The trace cache is out of scope: Peter will probably remove it, and the plan ignores it.564fcff (2026-09-21) moved it one 16 KB step to 1056 KB (configuration.h:146).buildRP2040L/PicoMite.bin is 822,244 B against the 816 KB offset.struct option_s is exactly 896 bytes.configuration.h:109,200,326). MAX_PROG_SIZE equals the heap, except that every RP2040 PicoMite build (PICO, PICOUSB and PICOMIN) fixes it at 120 KB (:393-397). That is the whole heap on PICO, but less than the heap on PICOUSB (124 KB) and PICOMIN (128 KB) (:356, 361).LOWRAM build with no trace cache and no label hashing. findvar, DefinedSubFun and the FOR, IF, NEXT, DO and INC handlers run from flash.MM.DEVICE$, MM.VER, the git hash, the CPU speed and OPTION LIST.This section follows one statement through the firmware. Every step was read in the code. The cycle cost of each step has not been measured on either board; Phase 0 exists to measure it.
The program runs directly from its tokenised image in flash, read through the 16 KB XIP cache. On the PC3 the RAM library and the RAM slots sit in PSRAM behind the same cache (PSRAMbase 0x11000000, configuration.h:524).
T_NEWLINE and a skip byte (MMBasic.h:98-107, written at MMBasic.c:1698-1720).00 00.The same image is the source for LIST, EDIT and SAVE, which is why it stays text.
MMBasic.c:1834-1859), once per line: set CurrentLinePtr, write the TraceBuff ring, test TraceOn, and step over the line number and label.1879-1881): skipelement(nextstmt) walks every byte to the terminating 0, including any trailing comment. ExecuteProgram uses the skip byte only to jump over comment-only lines (1863-1875).1890-1917): commandtbl_decode, then commandtbl[].fptr(). The table is const, so it is in flash. A user SUB name goes through FindSubFun and then DefinedSubFun. setjmp runs only while ON ERROR SKIP, IGNORE or RESTART is in effect, that is while OptionErrorSkip is not 0 (1925-1938; Commands.c:8546-8575).getargs/makeargs copy it into a 384 B stack buffer, calling strchr on each top-level byte (MMBasic.c:5551). A bracketed argument or function call is found with getclosebracket and copied in one memcpy (5600-5607).= with a tokenfunction table lookup on each byte (an inline macro, MMBasic.h:227-230; Commands.c:1787-1788), then skipvar, then findvar on the left side (Commands.c:1818).evaluate calls getvalue for the first operand, then doexpr while an operator follows (MMBasic.c:2889-2891). doexpr calls getvalue for each next operand and recurses on precedence (3066, 3146).
MMBasic.c:3336-3398).3135).findvar upper-cases and FNV-hashes each character, then takes % maxglobalvars at run time (4689-4701). Inside a SUB it probes the local table before the global one.name(. FindSubFun runs first (3277-3279), so every array read asks "is this a function?".
subfun[] one entry at a time (2027-2028).getargs copy, a bracket re-scan and skipvar.T_FUN, 3245-3253; Memory.c:1852-1860): getclosebracket, then a zeroed 256 B temp page, then a memcpy of the argument text. The page is freed at the end of the statement. Functions with no arguments, such as TIMER and PI, skip the copy (3254-3256).MMBasic.c:1940-1952):
ClearTempMemory if anything was allocated.1944).CheckAbort() and then check_interrupt():
CheckAbort calls ProcessWeb(1) on every statement on WEB builds (PicoMite.c:2682-2687), and HDMIWEB is one (CMakeLists.txt:798-806). ProcessWeb reads time_us_64, walks the PCBs, and polls cyw43 every 100 calls or every 1 ms (net/WiFi.c:295-345).routinechecks calls time_us_64() and returns if less than 100 µs have passed (PicoMite.c:893-901).check_interrupt calls ProcessTouch on every statement while GUI controls exist. Otherwise it returns unless InterruptUsed is set. Once any interrupt has been set, checkdetailinterrupts runs on every statement until END (MM_Misc.c:10558-10586). That is a 350-line function in flash (10209-10557) that tests every interrupt source in turn.Commands.c:5121-5235):
getargs, which trims argbuf in place (5130-5133).mystrncasecmp at every byte offset of that NEXT's argument list (5198-5235).NEXT itself is a pointer compare (5278-5284). An inner loop therefore re-scans its body once per outer iteration.
5378-5394). SELECT walks the arms on every dispatch (4729-4912).findline is a linear walk on both chips (MMBasic.c:3547). findlabel is hashed on the RP2350 (3742) and linear on the RP2040 (3802).DefinedSubFun, 2232-2709):
makeargs runs over the caller's argument list and over the definition's parameter list (2392, 2398).skipvar, CheckIfTypeSpecified (2527), and findvar(V_LOCAL).2370-2371). The RP2040 allocates and zeroes 75 × 29 + 512 = 2,687 B per call (2376-2385).ExecuteProgram (2679), which copies the jmp_buf on entry and again on exit.ClearVars walks the whole hash list (6356+).ExecuteProgram, the evaluator, findvar, the operators and the hottest handlers are RAM-resident. checkdetailinterrupts and most fun_ routines run from flash; fun_sin is at 0x1000cbfc in the HDMIUSB map (buildRP2350L).findvar and DefinedSubFun (MMBasic.c:4643, 2227);cmd_for, cmd_if, cmd_else, cmd_next, cmd_do and cmd_inc (Commands.c:5097, 3659, 3947, 5256, 5327, 909).PicoMite.c:3758-3761); the RP2040 VGA is always at CLKDIV 4 (CMakeLists.txt:588-591, PicoMite.c:3825-3827).| Work | Where | How often | Fixed for the RUN? | Resolved once today? |
|---|---|---|---|---|
| Find the end of the statement | MMBasic.c:1881 | Every statement, every byte | Yes (its length) | Per line, and only to skip comment lines |
| Decode and dispatch | 1890-1917 | Every statement | Yes | No |
| Poll for events | 1940-1952; PicoMite.c:2682-2687, 893-901 | Every statement | Almost always nothing happened | No |
| Split the arguments | makeargs | Nearly every command | Split points, yes | No |
| Resolve a name | findvar 4689-4701 | Every reference | Which name, yes; which storage, no | No; globals never move once created |
Ask whether name( is a function | 3277-3279 | Every array read | Yes | No |
| Convert a literal | 3336-3398 | Every evaluation | Yes | Tried, no gain (§3.4) |
| Copy built-in arguments | 3245-3253 | Every call of a function that takes arguments | Yes | No |
| Find NEXT, LOOP or a CASE arm | Commands.c:5198-5235, 5378-5394, 4729-4912 | Every loop entry; every SELECT | Yes | FOR and DO iterations and the IF jump table |
| Find a label or line | MMBasic.c:3547, 3742, 3802 | Every GOTO/GOSUB | For literal targets | RP2350 labels only |
| Parse a SUB's parameter list | 2392-2532 | Every call | Yes | No |
| Allocate and clear call scratch | 2370-2385 | Every call | Size known | No |
A=A+1, are estimates.docs/option-profiling-cache.md:300-311).
CMakeLists.txt:297-309), and on that build cmd_next is in RAM. On COM7 it is in flash.Split the interpreted time as T = W + O: the work itself plus the interpreter's overhead. mmb2csub converts about 132 of the 166 routines in its corpus (mmb2c's tests/, plus se.bas, julia.bas and knivd.bas). The same routine therefore exists interpreted and native, and T_native is a measured floor for W.
| Routine | Board | Interpreted | Native | O = 1 − Tnative/Tinterp | Source |
|---|---|---|---|---|---|
| Graphics-heavy routines | both | — | — | ≈ 0.75 (4x) | Peter |
| Julia set | RP2040 | 119 s | 5.9 s | 0.95 | docs/mmb2csub.md:33-34 |
| Julia set | RP2350 | 86 s | 2.5 s | 0.97 | same |
| Exile player kernel (hand-written C CSUB) | PC3 (clock not recorded) | 9.8 ms/tick | 0.1 ms/tick | ≈ 0.99 | Exile_Port_Design_Review.html:405 |
solar_eclipse.bas, sefunc as CSUB | PC3, 378 MHz | ~12.5 s (14.6 s on HDMIWEB with WiFi live) | 2.5 s | ≈ 0.80 | Peter |
solar_eclipse.bas, sefunc as CSUB | RP2350B, 252 MHz | 13 s | 2.7 s | 0.79 | docs/mmb2csub.md:670 |
What this implies:
This gives the yardstick used everywhere below. It is measured per board, within one build and one condition:
Removing 10% of the overhead counts the same on a 4x routine as on a 35x one. The floor needs two caveats:
docs/mmb2csub.md Appendix A).The sefunc CSUB barely scales with the clock: 2.7 s at 252 MHz and 2.48 s at 378 MHz (docs/mmb2csub.md:668-672). Between those two clocks the flash clock fell from 126 to 94.5 MHz while the CPU rose 1.5x. The interpreted remainder, flash fetch or the CallTable's double routines could each explain that. The plan does not chase it (flash timing is confounded by the QMI cache); it takes the floor as measured, on the same build as the interpreted time.
On the same chips, MicroPython written to mimic BASIC is up to about 30% faster than MMBasic (Peter's figure). Peter's MicroPython is his own PC3 port (\\wsl.localhost\Ubuntu\home\peter\src\micropython\ports\rp2, board PICO_COMPUTER_3). It uses double-precision floats and arbitrary-precision (MPZ) integers, so it does the same arithmetic as MMBasic (mpconfigboard.h:24-26; DEVELOPMENT_NOTES.md §1). On the anchor, the line-faithful port tests/solar_eclipse.py takes about 8.8 s against MMBasic's 12.5 s at 378 MHz on the same chip, with digit-identical output. That removes (12.5 − 8.8)/(12.5 − 2.5) ≈ 37% of the overhead.
py/compile.c).LOAD_FAST is fastn[-n] plus a NULL check, and an unassigned slot is NULL.for over range compiles to an inline counter, and const() is folded.DEVELOPMENT_NOTES.md says so too).MICROPY_HW_FLASH_MAX_FREQ), against MMBasic's 94.5 MHz. Its VM core is in RAM and its bytecode is in RAM.sin is software double precision. The ulab variant, with the series vectorised, still takes 7.4 s, and its README puts the remainder down to sin.LOAD_NAME/STORE_NAME dictionary operations, and builtins first miss in the globals.nlr_push on every call.Implication. A suffix-typed BASIC that adopts these resolution ideas without the dynamic object model has a higher ceiling than 1.3x. An Amdahl projection from the 300% overhead: cutting the overhead by 5x would give about 2.5x on graphics-heavy code and 4-5x on compute kernels. This is unverified.
A consistency check, also unverified. An assignment costs about 1,900-2,270 cycles in MMBasic (6 µs at 315 or 378 MHz), against a few hundred for MicroPython's x=x+1. That predicts far more than 1.3x on scalar code. The arithmetic is the same, so precision does not explain the gap. Two things could:
sin on an anchor with about 450 SIN( calls in its text.Either way, MicroPython carries costs MMBasic does not have and still wins. That points at MMBasic's operand handling, not at its arithmetic.
The E pairs in Phase 0 settle it.
MicroPython compiles in RAM. The whole-module parse tree is held until every scope has compiled, so peak RAM is O(source). It also keeps bytecode in RAM, for .py and for .mpy (persistentcode.c copies it). Only frozen bytecode, or ROMFS (off by default on rp2), runs from flash. An unverified anecdote: a 100 KB .py ran out of memory compiling on a Pico W, and a 28 KB .mpy used 60 KB of heap.
MMBasic today uses no RAM for program text; only small indexes, such as the IF table and the sub tables, live in RAM. Any compiled form of MMBasic must therefore be written when the program is saved, into flash (or into a RAM slot while testing), beside the text that LIST/EDIT/SAVE round-trip. BASIC can compile line by line with a forward-jump fixup list, so it needs no parse tree.
Do not adopt: dictionary globals, boxing, GC, compiling into RAM, or whole-module parse trees.
MMBasic.h:98-107; MMBasic.c:1698-1720). ExecuteProgram uses it only to jump over comment-only lines (1863-1875). Every executed statement is still scanned byte by byte (1881).forptr and nextptr sit on the FOR stack, and loopptr on the DO stack (Commands.c:5391, 5492-5496), so NEXT and LOOP jump straight back. What remains is the forward scan each time a FOR (Commands.c:5198-5235) or DO (5378-5394) is entered, which for an inner loop is once per outer iteration.Commands.c:3250-3565). It lives in the heap.funtbl (MMBasic.c:3654). The RP2040 does not have this.So block partners are mostly done already. The remaining gaps are the entry scans and SELECT.
Peter stored numeric literals in binary behind a pretend function token, and there was no noticeable improvement. That fits the model. Converting a literal is a small part of a statement whose evaluator still walks the text around it, hashes the names and copies the arguments.
The lesson is general: removing one piece of text parsing buys little while the evaluator still re-walks the text. The gain arrives only when the walk itself stops. Symbols (S5-S6) start that, and a compiled stream (Route B) finishes it.
OPTION TRACECACHE ON [size[,flags]] is off by default and exists only on CACHE builds (CMakeLists.txt:297-309): HDMIWEB has it, the RP2040 VGA does not. It compiles the right-hand sides of LET, IF, INC, DO and SELECT, and caches jump targets. Entries are keyed on the statement's source address, through a golden-ratio hash with up to 8 probes (MMtrace.c:407-452). A cached statement replays a small typed stack machine.
| Case | Result | Source |
|---|---|---|
| RP2040 PICO build, 252 MHz, 1M-iteration DO UNTIL | 3,822 ms → 2,204 ms. The equivalent FOR takes 771 ms. | option-profiling-cache.md:300-311 |
| Elite on the PC3, 128 → 256 slots | Misses 18,618 → 757; frame 41.9 → 37.7 ms, about 10%. The cache was on in both runs. | commit d1b4b96 |
| ExilePhysicsBench | Halves the integrate loop and changes nothing else | Exile_Port_Design_Review.html:372 |
| Exile player kernel | 9.8 → 11.5 ms, slower; wrong result with CollTiles cached | Exile_Port_Design_Review.html:405 |
It fails for six reasons.
findvar and the evaluator for one statement. It does not skip the pre-scan, the dispatch or the tail. IF's getargs runs before the cache is even checked (Commands.c:3694 against 3722).TraceCacheTryExec is a stub (MMtrace.c:3376-3381).g_local_frame_gen (MMtrace.h:50-59), changes on every frame entry and exit.MMtrace.c:822-878).findvar. That is why the Exile kernel got slower.argbuf, which runs a single-line THEN a ELSE b (Commands.c:3787), and tknbuf, which runs EXECUTE and the prompt, present one key for different code.physics.bas:450 and :452 are both If cy Then X = a Else X = 0. That is the probable cause of the CollTiles result. This comes from reading the code; it has not been reproduced.MMtrace.c:15-18, 1482-1553). Real programs spend their time on calls, array traffic and command arguments ("calls and array traffic, not tight arithmetic", design review :372). It does not cover calls to user SUBs and FUNCTIONs, or command arguments.^ is right-associative;pow();>> is an arithmetic shift;NOT 0.4 gives 1;MMtrace.c:3497, 3847, 4135). LET and IF round, as the interpreter does;Commands.c:5415-5450) is gated only by #ifdef CACHE. On COM16's HDMIWEB build it is live today, with no option set.MMtrace.c:79-83, 373-377). What grows with the code is the number of slots needed to stop missing. Each slot is 216 B (MMtrace.c:115, 314-329), so Elite's 256 slots take about 55 KB of heap.buildRP2350L) shows 10,038 B of .time_critical and 9,138 B of .text, 19.2 KB in all.MMtrace.c:74-77) is not wired up. NEW, RUN and EDIT drop the whole cache (Memory.c:2010; misc/Editor.c:629), and the file-manager relaunch frees it (misc/Editor.c:4440), but DIM, ERASE, OPTION BASE and OPTION EXPLICIT do not invalidate it.| Rule | What it requires |
|---|---|
| R1 Identity | Key work on something carried in the program bytes (a symbol id), or on a small stable integer the interpreter already has (a subfun index, an offset in the immutable stored image). Never on a buffer address. |
| R2 When | Compute static facts when the program is saved. Change dynamic bindings only at the events that change them: a variable created or destroyed, ERASE, CLEAR, RUN, CHAIN. Each event gets one reset point and a golden test. No global frame counter, and nothing re-derived per execution. |
| R3 Replace, don't prefix | A fast path replaces the slow path it shortcuts. The only miss allowed is a name that is genuinely unbound, and a miss costs no more than today. |
| R4 One semantics | Reuse evaluate, doexpr, the op_* functions and the runtime pairing scans. Anything new ships with a shadow mode that checks it against the old path. |
| R5 Memory | RAM is fixed, or O(unique names + call depth). Facts whose size grows with the code live in the stored image. The image may not grow beyond measured slack below MAX_PROG_SIZE, and today's largest programs must still run. |
| R6 Coverage | Target a bucket the measurements show is large, including polling and argument passing, not only expressions. |
A symbol id identifies a name as it appears in the program text. It does not identify a variable, and it does not identify a statement address.
findvar runs exactly as today and its result is recorded against the id. Implicit creation, conditional DIM and LOCAL, OPTION EXPLICIT and recursion all behave as they do now.argbuf carries its own ids.The trace cache bound results to a statement address and invalidated them globally on every frame change. Ids bind to a name and change only on the events listed above.
main, because mmb2csub is far more powerful. Nothing below measures it, keeps it or tries to match it; it appears in this document only as the lesson above. Removing it frees 19.2 KB of text on the CACHE builds, about 10 KB of it RAM-resident (HDMIUSB map in buildRP2350L). It also removes the always-on DoFast path and its truncation bug (§7).Measure on both boards, lead with one real program, compare every interpreted number with its native twin, and state in advance which result would kill each hypothesis.
solar_eclipse.basBas/solar_eclipse.bas is byte-identical to Testfiles/solar_eclipse.BAS. It is 3,213 lines and 70 KB, of which about 44 KB is non-comment text, in 33 routines. It times itself: Timer =0 is at line 224, and "Time taken" is printed at line 242. The key routine is Sub sefunc(x, fx) at line 248. With its callees it covers 15 of the 20 profiled routines and 99.8% of the calls (docs/mmb2csub.md §9).
Reference inputs (Peter), typed in this order at the five prompts: 12,1,2000 · 39,40,36 · -104,57,12 · 1644 · 30. They give the 25 December 2000 eclipse. Golden output: JD 2451904.14541560, .19690359 and .25420764, the same to the last digit in MMBasic, in the CSUB version and in Peter's MicroPython port (boards/PICO_COMPUTER_3/tests/README.md in that tree). Every run uses these inputs and is checked against these digits.
| Configuration | Board and condition | Time | Source |
|---|---|---|---|
| MMBasic, interpreted | PC3, 378 MHz, reference inputs; build not recorded (July 2026) | ~12.5 s | Peter's MicroPython tests/README.md |
| MMBasic, interpreted | PC3 on COM16, HDMIWEB, 378 MHz, WiFi live, HDMI running | 14.6 s | Peter, 2026-09-24 |
| MMBasic, interpreted | RP2350B, 252 MHz | 13 s | mmb2csub.md:668-672 |
sefunc as a CSUB (--library --lean, 37 KB blob) | RP2350, 378 MHz | 2.48-2.5 s | Peter; mmb2csub.md |
| sefunc as a CSUB | RP2350B, 252 MHz | 2.7 s | mmb2csub.md |
| sefunc as a CSUB | RP2040 PicoMiteVGA | 5.7 s | mmb2csub.md |
MicroPython, tests/solar_eclipse.py (line-faithful port; double floats, MPZ integers) | PC3, 378 MHz (flash at 63 MHz, divider 6) | ~8.8-9 s | Peter; MicroPython tests/README.md |
MicroPython, tests/solar_eclipse_ulab.py (series vectorised with ulab) | PC3, 378 MHz | ~7.4 s | same README: the rest is software double sin |
| MMBasic, interpreted | RP2040 VGA on COM7 | not measured | — |
What each ratio means:
mmb2csub.md:670) gives 1 − 2.7/13 = 0.79. Against today's 14.6 s on HDMIWEB it is 0.83, once the CSUB has been re-measured on the same build. This is a lower bound, because the CSUB run still includes the interpreted remainder. The overhead is about 22 ms per sefunc call (10.5 s over 474 calls).ProcessWeb on WEB builds, HDMI scan-out contention and code placement. COM16 can take any RP2350 build, and all of them auto-configure as a Pico Computer 3 (Peter), so 0b measures the spread directly.What the anchor exercises (from reading the source):
Option default float (line 10) and Option base 1 (line 12). Most numeric variables carry no suffix and take FLOAT from OPTION DEFAULT. But loop counters in six routines are % integers, line 26 declares i and j AS INTEGER, and a few names carry $. Typed operations must take their type from OPTION DEFAULT, DIM and LOCAL, not only from suffixes.findleap, utc2tdb and jdfunc are called about 1,455 times each (mmb2csub.md §9).dtr with 101 uses and pidiv2 with 167. Every read probes the local table first.SIN( and 25 COS(, and each call goes through the T_FUN temp-page path.moon() (lines 974-1531) is about 20 KB of non-comment text, more than the 16 KB XIP cache. Its text streams from flash on every call and evicts flash-resident handlers such as fun_sin.Timer =0 (getdate at 199, observer at 205, ndays at 223; the input routines are at 1884-1959). Feeding them over serial does not touch the timed region.Bas/knivd.bas:9-16);Log with every anchor run:
MM.VER and git hash;MM.INFO(CPUSPEED);MM_Misc.c:6723-6768). The heap shrinks with the mode only on the full HDMI builds (PicoMite.c:3847-3877), not on HDMIWEB;OPTION LIST;mmb2csub.md §9).About 1-2 weeks (a guess). No behaviour change.
Peter's rule is to ask before spending a build cycle on something he may already know. The questions are in §8. Already answered by Peter:
MM.INFO(CPUSPEED)). A non-USBKEYBOARD build also brings up a CDC console on the USB lead used for flashing.\\wsl.localhost\Ubuntu\home\peter\src\micropython\ports\rp2 (board PICO_COMPUTER_3), runs on a board on COM9. That board is a "PC2": the same hardware as the PC3 except that it has no WiFi module. So the like-for-like comparison is MicroPython on COM9 against MMBasic's HDMIUSB build on COM16, at the same CPU clock. Its tests/solar_eclipse.py is a line-faithful port of the anchor.mmb2csub/user-tools/ (mmb2csub.py, mmb2c.py, armcfgen.py). The corpus is at \\wsl.localhost\Ubuntu\home\peter\src\FUZIX\Applications\mmb2c\tests: 112 .bas programs and 90 .expected outputs. Claude can build every CSUB twin and run the goldens without Peter.MM.INFO(CPUSPEED) and set with OPTION CPUSPEED nnnn where the build allows it (the HDMI builds take their clock from OPTION RESOLUTION). Which build becomes the reference for the gates is decided from these results, and HDMIWEB with WiFi connected against disconnected. The board stays the same and only the build changes, so the differences price HDMI scan-out and the per-statement ProcessWeb (H5) directly. Reflashing only; no code change.OPTION PROFILING ON, as it is today, gives the command mix, the local/global split of findvar calls and SUB call counts, plus per-SUB times on CACHE builds such as HDMIWEB (0e). The anchor ends with End (line 243), so the report prints. Timed runs are made with profiling off.Every micro-benchmark runs about 100k iterations. The empty-loop baseline is subtracted, the median of 5 is taken, and runs are interleaved ABAB. Every file ends with END and prints BENCH name iters us cpu_mhz device.
s%=(s%+i%) AND 65535.x=x*1.000001+0.5.tests/solar_eclipse.py is the anchor's twin. The MMBasic side of every pair runs on the HDMIUSB build on COM16 at the same CPU clock: the same hardware, no WiFi on either side. Each test is written twice, at module level (which mimics BASIC) and inside a def, and run from .py and frozen.pc3.py on COM16 and COM7, with serial lines paced at 20 ms or more. Never NOCHECK in an unbounded loop.Memory.c:2147)..expected outputs, run on the board.Testfiles/ (SelectCaseTest.bas, doloop_test.bas, SubParamTest.bas, ErrLineTest.bas, ArrayCopyTest.bas, ArrayFuncTest.bas, MathClampTest.bas, StructMemberPassTest.bas) are a starting point. It covers:
^, NOT, >> and NaN;Bas/, PoP and Elite, needed before any format step.What it records today:
OPTION PROFILING ON|OFF is parsed at core/MM_Misc.c:5327-5347. ON allocates the counters from the heap (ProfilingAlloc, core/MMBasic.c:171-201) and OFF frees them (ProfilingFree). Every increment is gated, so when it is off it costs only the gate tests.MMBasic.c:1896-1897), the number of user SUB dispatches, findvar calls split into local and global, and call counts per SUB.MMBasic.c:1913-1915, 2504, 2705; Commands.c:4292-4299). These are taken in EnterLocalFrame/LeaveLocalFrame, inside #ifdef CACHE (MMBasic.c:235-311), so they exist only on CACHE builds: HDMIWEB has them, COM7's VGA build does not.[PERF] report is printed only by cmd_end (Commands.c:4161-4380), so the program must reach END.Extensions. Each is selected by an argument to OPTION PROFILING, so it needs no new token and no option_s byte. They are added one at a time, as the hypotheses need them:
CurrentLinePtr, and a per-command time histogram, both sampled in the 1 ms timer_callback (PicoMite.c:2332). Sampling also catches time spent in built-in functions and operators;skipelement, getclosebracket and makeargs; temp pages allocated; FindSubFun misses; calls to, or time in, CheckAbort and check_interrupt; LOCAL arrays created;Profiling perturbs timing. Timed runs are made with profiling off, and profiled runs are used only for shares.
Fallbacks, only if the extended PROFILING data leaves H1 open: a PC/LR sampler on a spare hardware alarm, and SWD DWT_PCSR sampling if Peter already has a probe working.
A no-op size-change rebuild, measured on the anchor itself and on knivd. A claim on a macro benchmark must exceed twice this floor, or be proven on the step's own micro-benchmark.
A build with findvar memoised by program-text address and no invalidation. It gives an upper bound for S5-S6 before any real work is done.
It must be restricted to globals and scalars, or keyed on the address plus a frame serial. Otherwise sefunc's per-call LOCAL arrays would hand back freed pointers: wrong answers, or a crash. The build is never shipped.
Moved out of Phase 0: the VM-as-CSUB prototype. It only informs the Route B decision, and it is confounded (M0+ code on the M33, big blobs running from XIP, doubles through the CallTable). It now sits at G3.
About 1 week (a guess). Both boards.
The output is one table per workload per chip:
Each of H1-H10 is marked confirmed or killed by the thresholds below.
| # | Hypothesis | Experiment | Confirm | Kill |
|---|---|---|---|---|
| H1 | Operand work in the evaluator is at least half of the overhead in compute code. That covers the getvalue/doexpr recursion, marshalling through globals, and re-walking names and brackets. | Extended OPTION PROFILING (line and command time histograms, 0e counters) on the anchor, julia and fixed-work knivd; E1 − E0 per statement; the 0g ceiling separates lookup from evaluation | Evaluator + lookup + text scan ≥ 50% of the overhead; (E1 − E0) ≥ 3 × E0 per statement | Evaluator core under 25% |
| H2 | Name resolution is 20-40% of statement time | E1b, 1- against 30-character names; top level against inside a SUB against recursion depth 10; a CONST read inside a SUB (the anchor's pattern); COM7 y=a(i) with 0 and 200 dummy SUBs; the 0g ceiling | Slope ≥ 10 cycles per character (PC3) or ≥ 15 (COM7); ceiling ≥ 15% on the anchor | Slope under 3 cycles per character and ceiling under 8% |
| H3 | Call machinery, including LOCAL arrays created per call, dominates code built from SUBs | E5: 0, 3 and 6 parameters, with and without AS INTEGER; a sefunc-shaped SUB with 7 LOCAL arrays against the same using global arrays; call counts from PROFILING | Definition-side parse plus zeroing ≥ 25% of call cost; call bucket ≥ 20% of the anchor or game samples; LOCAL-array form ≥ 1.3x | Definition side under 10% of call cost, and LOCAL-array creation under 5% of a call |
| H4 | Marshalling the arguments of built-ins and commands is the 300% in firmware-heavy code | x=SIN(y) minus x=y; SIN nested 1 to 5 deep; PIXEL x,y,c against PIXEL 10,20,1; LINE and BOX likewise; temp pages per statement | Variable form ≥ 1.5x the literal form; cost grows faster than linearly with nesting | Variable form under 1.2x the literal form |
| H5 | The per-statement tail is 10-25% of simple statements. It is larger on HDMIWEB, where ProcessWeb runs on every statement, and larger with any interrupt armed or GUI controls present. | 0b's WiFi and NOCHECK A/Bs; E0 and A=A+1 under NOCHECK; each again with SETTICK armed and with GUI controls allocated; the 0e CheckAbort/check_interrupt counter | E0 ≥ 15% faster under NOCHECK; anchor ≥ 3%; SETTICK case ≥ 8%; WiFi-off delta ≥ 2% on the anchor | Anchor under 2%, SETTICK under 4%, WiFi delta under 1% |
| H6 | Flash fetch through the XIP cache is a large cost. | Not pursued. Peter: flash timing is not very relevant, because its effect is confounded by the QMI cache. Placement effects are already known to be about 4%, and every gain must beat that floor. | ||
| H7 | The pre-scan over trailing comments and spaces costs at least 5% on commented code | E1b: A=A+1 against the same with a 40-character trailing comment and with 40 spaces; DATA with a 60-byte list (a cmd_null statement) | ≥ 5% per 40 bytes (about 5 cycles per byte) | Under 2% |
| H8 | Block-entry scans matter in real programs | Nested FOR, inner count 2 against 100, body 40 against 400 B; SELECT with 30 arms, first against last; COM7 GOSUB to a label on line 10 against line 1000; histogram share of FOR, DO and SELECT lines | Block and jump lines ≥ 3% of macro time, or the COM7 label case ≥ 2x | Under 1% on every macro |
| H9 | Temp-memory churn is ≥ 10% of string and built-in code, and depends on how full the heap is | E4 with the heap empty against 80% full | ≥ 1.15x slower with the heap full | Under 1.05x |
| H10 | MicroPython's lead comes from scalar, array and call mechanisms, not from work done in C | Pairs E0-E9, module-level and def versions of each Python test; Peter's own 30% pair first | E1, E3 and E5 ratios much larger than E0's; E9 about 1 | Ratios flat across the tests (a uniform dispatch cost). Then H5 and H7 move up. |
Five strategies were written independently and then judged. None is adopted as written. The recommended programme takes the measurement discipline from one, the identity layer from another, a binding mechanism from a third, the yardstick from a fourth, and the design rules for a later compiler from the fifth.
Profile first. Then remove structural repetition inside shared primitives, using tables built at PrepareProgram as the IF table is:
RP2040: the tables compete with the 100 KB heap, and findvar and the FOR, IF, NEXT, DO and INC handlers run from flash. RP2350: a 12 KB binding cache.
Kept: the whole Phase 0/1 skeleton and the attention word.
Changed:
One pass at RUN builds a side structure in the heap:
Cost: by its own estimate about 0.4 KB of heap per KB of program for the spine and links, and 0.9 KB per KB with sites. The speed-up disappears exactly on large programs.
Kept:
SUB bodies and hot loops compile to 16-bit typed wordcode in the heap. At every statement boundary, the state is the same as the text interpreter's.
Highest ceiling: 2.5-7x on kernels (its estimate).
Costs:
Kept as Route B's design rules:
subfun index;It is only acceptable as a stream written at save time into a slot, never compiled into the heap.
This is not an interpreter speed-up. It contributes the yardstick, removed(X) against the CSUB floor, and the mmb2c corpus as a semantics oracle.
Its firmware items are cheap and touch only CSUB paths, at about 150-300 B each (its estimates):
subfun.Offer these separately from this plan.
Repeated names become 2- or 3-byte references into a name block kept inside the image. The block holds each name's spelling, a hash16 identical to the text path's, and a static binding for every SUB, FUNCTION, CSUB and label. The tokeniser emits ids only in commands flagged as audited.
Costs:
g_vartbl: 26,880 B on VGA and 47,104 B on the PC3.Speed alone is small: 3-20%, its estimate. But it is the identity layer every later step needs, and the only proposal that makes the stored program smaller.
Its gap: the speed-up rests on findvar still probing. Shallow binding fills it.
On every axis 10 is best. For compatibility risk, 10 means lowest risk; for effort, 10 means least effort. These are judgements of each strategy as written.
| Strategy | Gain | RP2040 feasibility | Program capacity | Compat. risk | Effort | Incremental | Measure first | Total /70 |
|---|---|---|---|---|---|---|---|---|
| measure-first | 5 | 8 | 7 | 8 | 8 | 9 | 10 | 55 |
| native | 3 | 9 | 6 | 7 | 9 | 9 | 8 | 51 |
| format | 4 | 7 | 10 | 5 | 5 | 8 | 8 | 47 |
| prepass | 6 | 4 | 2 | 6 | 5 | 7 | 8 | 38 |
| bytecode | 9 | 3 | 4 | 3 | 2 | 6 | 7 | 34 |
Bytecode's capacity score of 4 is for compiling into the heap. Moved to a stream written at save time into a separate slot, as Route B does, it costs nothing from the program area or the heap beyond names and call depth.
"Heap" here means heap at run time, which comes out of the user's variables. Today both boards are limited by the source text that LOAD holds in the heap: GetTempMemory(EDIT_BUFFER_SIZE - 2048) (misc/FileIO.c:4345; configuration.h:561). That is about 93 KB on the VGA and about 137 KB on the PC3 at 640 pixels wide, less at the wider HDMI modes. A smaller image becomes a bigger program only once LOAD tokenises while it streams the file.
| Strategy | RAM per KB of program | Program flash per KB | Largest program, RP2040 VGA | Largest program, PC3 HDMIWEB |
|---|---|---|---|---|
| measure-first, as written | Structure table 16 B per block statement (about 0.1-0.2 KB/KB); binding cache fixed at 3 KB (VGA) or 12 KB (PC3); about 8 B per parameter | 0 | Limit unchanged, but a 90 KB program loses roughly 12-21 KB of variable heap unless the tables switch themselves off (estimate) | Limit unchanged; roughly 26-40 KB of heap on a 140 KB program (estimate) |
| prepass | About 0.4 KB/KB (spine, links, calls); about 0.9 KB/KB with operand sites (its own estimate: 12 KB and 28 KB for a 30 KB program) | 0 | A 90 KB program needs about 36 KB of heap, or about 80 KB with sites. The speed-up is lost on large programs. | A 140 KB program needs about 56 KB, or about 126 KB of the 144 KB heap |
| bytecode, as written | 1.0-1.5x the compiled statements' size plus records, capped by a budget (8-16 KB VGA, 32-64 KB PC3) | 0 in the image; firmware +15-25 KB | Limit unchanged; coverage stops at about 5-10 KB of hot code | Limit unchanged; coverage stops at about 20-40 KB of hot code |
| native | 0 for the interpreter | A converted routine costs about 3.4x its blob in program memory: the hex text (about 2.4x) plus the binary built from it (1x). For sefunc's 37 KB blob that is about 121 KB (FileIO.c:3830; mmb2csub.md A.8). In the single library slot it costs 1x (mmb2csub.md §9) | Interpreter unchanged; conversions are opt-in | same |
| format | Fixed, under 1.2 KB; +2 B per name for the optional slot cache | Negative on large programs. Emulator run for this review: Elite 110.4 → 101.5-105.5 KB (−8.1% to −4.4%); PoP player.min.bas 137.7 → 120.4-124.4 KB (−12.6% to −9.7%), or −14% to −17% with indent runs. The range depends on whether the block carries a 4 KB lookup index. The anchor comes out +0.6% to +2.1% (21% comments, 16% names). | About 93 KB of source, unchanged until LOAD streams; then the image saving becomes capacity | About 137 KB of source; after streaming LOAD, roughly 10-17% more PoP-style source in the same image |
| Recommended: Route A in the image + Route B stream in a slot | About 4-8 B per unique name, plus a fixed array per variable slot and frames proportional to call depth. Elite's 876 names cost about 3.5-7 KB (2.4-4.9% of the PC3 heap). The IF table leaves the heap. | Symbols shrink large images; descriptors, partner tables and a small section header add to them. Each step measures the net change against the slack below MAX_PROG_SIZE. The Route B stream takes nothing from the program area. | Unchanged (about 93 KB of source), provided the stream is no larger than the image (a G3 condition) | Unchanged (about 137 KB); PICORP2350 likewise |
The emulator is measure.py in the session scratchpad: an approximate re-implementation of tokenise(), good to about ±2-3%. It was re-run for this document on Bas/elite/elite.bas, Bas/sandglass_tools/player.min.bas, Bas/solar_eclipse.bas and Bas/exile/*.bas.
Measure; characterise; then make the text cheaper one mechanism at a time, with every static answer stored in the image. Build a compiled stream only if Route A leaves a large, measured overhead and a prototype shows the stream can remove it.
ProcessWeb is the cost.Every step runs the goldens, the micro and macro sets, the never-slower set and a capacity check, on both boards. The capacity check has two parts: the image size from "Saved nnn bytes", and MM.INFO(HEAP) free at RUN for the largest corpus programs.
| Step | What | Targets | Capacity cost | Boards |
|---|---|---|---|---|
| S1a | ProcessWeb moves behind the same 100 µs cadence as routinechecks, on WEB builds only. Verified with the WebMite TCP tests. | H5, priced directly by the WiFi A/B | 0 | COM16 (HDMIWEB) |
| S1b | An inline raw-timer compare in front of the call to routinechecks. It keeps the 100 µs cadence exactly and removes the call, the register push and time_us_64 from most statements. | H5 (NOCHECK A/B) | 0; a few bytes of RAM code | Both |
| S1c | An interrupt pending word.
| H5 (SETTICK A/B) | 0 per KB; about 0.2-0.4 KB of flash (estimate) | Both |
| S2 | RP2040: cache the subfun[] count at PrepareProgram instead of counting on every call and every array read (MMBasic.c:2027-2028). | H2 (COM7 with 200 dummy SUBs) | 0 | COM7 first |
| S3a | An image-section container: an append-only trailer after everything the image holds today, including CSUB and font data, with a magic number and a directory.
| Enables S3b, S5 and S7 | A few bytes per image | Both |
| S3b | Call descriptors, written at save time into the image and indexed by subfun. Each holds the parameter names, types, BYVAL/BYREF and array flags. They replace makeargs over the definition (MMBasic.c:2398) and the per-parameter tests. | H3 | About 20-40 B per SUB in the image (estimate); 0 RAM | Both; Exile and PoP on the PC3 |
| S4 | Array indices are evaluated in place. evaluate already stops at , and ) (MMBasic.c:2908-2911). findvar is changed to return its end pointer as well, so skipvar is not needed. Today it returns only the value pointer (MMBasic.c:4657), and callers use skipvar. | H2, H4 (arrays) | 0 | Both |
| S5a | The name block and save-time dictionary are written, but no references are emitted yet. Round trip verified on every writer. | Infrastructure | Temporary image growth, measured | Both |
| S5b | Labels and GOTO/GOSUB/RESTORE targets become symbols, and llist decodes them. findlabel on the RP2040 becomes O(1). | H8 (COM7 label case) | Image shrinks slightly | COM7 first |
| S5c | SUB, FUNCTION and CSUB definitions, calls and name( become symbols with static bindings. This removes the FindSubFun hash, count and binary search. | H2, H3 | Image shrinks | Both |
| S6 | Variables become symbols with shallow binding, flagged one command at a time with LET first. Details are below this table. | H2 | About 4-8 B per unique name plus fixed per-slot arrays; image −4% to −13% on the largest programs | Both |
| S7 | A partner table in the image, built by the same runtime scan function.
| H8 | Image grows (unmeasured; charged against measured slack); IF-table heap freed | Both |
| S8 | Built-in arguments in place for an audited set of functions, starting with single-argument numeric ones (SIN and COS for the anchor). The temp page and memcpy are skipped. | H4 | 0 | Both |
| S9 | Only if H7 is confirmed: extend the existing skip byte, for example by making a trailing comment its own element so the comment-line fast path jumps it. | H7 | About 2 B per trailing comment | Both |
S6 in detail.
ClearVars and ERASE.ClearVars(0), CHAIN/RestoreContext, and LIBRARY LOAD ,RAM.findvar and then binds. This keeps implicit creation, conditional LOCAL, re-DIM and recursion exact.findvar on every hit and counts disagreements.Not in the plan unless the measurements ask for it. Four hand micro-optimisations are left out: a static argument pool, zeroing sized by argument count, an early exit from ClearVars, and a register-local skipelement. Each is likely below the ~4% placement floor on the anchor. One is allowed only when the histogram shows its bucket at 2x the floor or more. Caching the subfun count (S2) is different: it is algorithmic, O(N) to O(1).
564fcff, 2026-09-21). Removing the trace cache, which Peter plans for main, frees 19.2 KB of text on every CACHE build, VGAUSBRP2350 included. The RP2040 VGA has about 13 KB.Route B is a go only when all of these hold:
MAX_PROG_SIZE, and RAM stays O(unique names + call depth).The Exile kernel pair (9.8 ms against 0.1 ms) is a second floor. It checks that the anchor's result generalises to SUB-with-locals code.
tokenise(false) at PicoMite.c:4788). The compiler emits alongside it and appends a forward-jump fixup table, so there is no whole-program parse tree.CurrentLinePtr, TraceBuff) is derived from the instruction pointer where possible.op_* helpers.g_forstack, g_dostack and gosubstack. State is identical to the text interpreter at every statement boundary, so interrupts, ON ERROR, CONTINUE and EXECUTE work unchanged. S1c's pending word is the only per-statement test.Delivery, each phase with its own gate:
run_phystest.py, and the anchor improves.Where the stream lives while testing:
configuration.h:400-410; ImageSlotAddress, misc/FileIO.c:1071). Each slot is MAX_PROG_SIZE. The stream is rebuilt at each save or RUN and is lost at power-off. This avoids repeated flash burns.Where the stream lives once the concept is proven (in production, one of the user's flash slots while compiled mode is in use) is deferred until then. The capacity-optimal end state, a single executable image that LIST decompiles, is noted but not started.
| Activity | Peter | Claude | Hardware |
|---|---|---|---|
| §8 questions | Answers the remaining questions | Asks once | — |
| 0b no-build matrix | Runs it, or frees the board; sets the resolution (which sets the PC3's clock) and WiFi; flashes the build to compare | Writes the anchor harness copy with constant inputs; delivers it by TFTP (PC3) or paced serial (COM7); logs | COM16, COM7 |
| 0c/0d suites and goldens | Confirms each board is free; types RUN on any board he is using | Writes the BASIC, the Python and the goldens; drives only free boards through pc3.py | COM16, COM7; COM9 for MicroPython |
| 0e PROFILING extensions; 0f-0g builds | Says when the build directory is free; flashes, or approves flashing | One PROFILING argument per build, added as a hypothesis needs it, never concurrent with his builds; a host script that reads the machine-readable report | Both |
| Phase 1 and G2 | Decides with the data | Writes up the tables and a recommendation | — |
| Each Route A step | Reviews before landing; flashes; decides flash steps | One step, one build, one commit; goldens, benchmarks and capacity check; reports | Both; games on the PC3; labels and subfun on COM7 first |
| G3 | Decides go or no-go | VM-as-CSUB prototype; host-side stream-size encoder; census | Both |
| Route B, P1-P5 | Reviews each phase gate | Implements; shadow mode; RAM-slot runs on the PC3; batched flash-slot runs on COM7 | Both |
Effort, all guesses: Phase 0 1-2 weeks; Phase 1 about a week; S1-S4 a few days each including board runs; S5-S7 several weeks together; Route B 3-5 months part-time, the bytecode strategy's own estimate.
| Area | What can break | How the plan guards it |
|---|---|---|
| EXECUTE, the prompt, CALL by string | Code in tknbuf or argbuf; names that never appear in the program text | Nothing is keyed on a buffer address (R1). Symbols travel with copied bytes. Names absent from the text take today's by-name path. Console tokenising looks the name up in the block, or emits raw text. |
| ON ERROR SKIP/IGNORE | The per-statement setjmp (MMBasic.c:1925-1938), the skip count, and its save and restore across interrupts (MM_Misc.c:10529-10533) | Unchanged in Route A. Route B arms one setjmp per unit entry and keeps its program counter in memory. Covered by the golden suite. |
| Interrupts | A source that sets a polled flag but not the pending bit would lose an interrupt. Pin interrupts are sampled with ExtInp once per statement. | Polled classes are still tested on every statement. A shadow build counts misses. The nextstmt redirect and IRETURN are unchanged. The GPIO IRQ path is not touched. |
| Interrupt frame accounting | Interrupt entry does g_LocalIndex++ twice with no EnterLocalFrame (MM_Misc.c:10528, 10552). cmd_return calls LeaveLocalFrame (Commands.c:5957-5959). | S6 binding tests the live g_LocalIndex, which balances, and never uses the CACHE-only frame stack. |
| LIST, EDIT, SAVE, AUTOSAVE, XMODEM | Symbols or sections that do not round-trip | The byte round-trip golden runs before and after every format step. llist decodes symbols. Commands are flagged one at a time. |
| Every image writer | A writer that drops the new sections | SaveProgramToFlash (PicoMite.c:4716), SaveLibraryImage, FLASH SAVE and RAM SAVE must all carry them; each is tested. SaveLibraryImage writes both libraries: the flash library with base=NULL and the RAM library with base=rambase (MM_Misc.c:2419, 2370); its CSUB fix-up is at FileIO.c ~4280-4312. RAM SAVE is a whole-slot copy (FileIO.c:865-875). RAM RUN and RAM CHAIN run a slot in place and write nothing (FileIO.c:917-958). Images without the sections run the text path. |
| LIBRARY | The library lives in flash slot 3 or in the PSRAM RAM slot. LIBRARY LOAD ,RAM must be the program's first statement (MM_Misc.c:2302-2333). It does a RUN-style ClearRuntime(true), then PrepareProgram, then restarts the program (MM_Misc.c:2377-2380). | Library and program symbol blocks are merged, as IfTableBuild already handles two slices (Commands.c:3514-3527). The library load is a binding reset event, like RUN. |
| CHAIN; RUN "file" | CHAIN: SaveContext copies g_vartbl, g_hashlist and the heap to PSRAM or to /.vars on A: (Commands.c:4465-4470, 4508-4510). Then ClearVars(0) and InitHeap run, the new program is loaded and prepared, and RestoreContext copies them back (Commands.c:4643-4657). RUN "file" is a ClearRuntime reset with no saved context (do_run, Commands.c:2460). | Both are S6 reset events. g_vartbl is free while the new program loads, so it can be the save-time scratch under CHAIN too. The limit under CHAIN is the A: drive space SaveContext needs on boards without PSRAM, not the variable table. |
| CSUB ABI | CSUB addresses are offsets within the image (PicoMite.c:4705-4715). RunBasicSub exposes ExecuteProgram (PicoCFunctions.h:116, 268). mmb2csub wrappers take variable addresses. | Sections are append-only. The text path stays complete. Variable storage never moves. The CallTable is append-only. |
| TRACE, TRACE LIST, error lines | CurrentLinePtr and TraceBuff are set per line | Unchanged in Route A. Route B records point to the text. |
| Build variants |
| Both boards are measured on every step. Map sizes are checked per variant. Hot paths on the RP2040 stay at -O2/-Os. Buffers get explicit alignment. |
| PSRAM or none | The RP2040 has no PSRAM and no RAM slots. The PC3 spills large allocations to PSRAM (Memory.c:2147), and a PSRAM table or stream has different timing. | Side tables are allocated from SRAM explicitly. Route B is tested in a RAM slot and confirmed once from a flash slot. |
| Program capacity | The LOAD text buffer limits both boards (FileIO.c:4345). It shrinks with the screen width (configuration.h:561), and on the full HDMI builds the heap depends on the display mode too. | Capacity is checked on every step with the largest corpus programs. The image size is logged. |
| Old images and downgrades | Old firmware cannot read new images. PEEK(PROGMEM) users see the new bytes. | A magic number marks new images; images without it run raw. The change is documented. |
| Variable semantics | Structs, STATIC name mangling, OPTION EXPLICIT, ERASE tombstones, re-DIM, OPTION BASE, OPTION DEFAULT | Binding happens only through findvar. Dotted names and STATIC stay on the slow path at first. A golden test covers each invalidation event. |
These come from reading the code. Each has a short board repro in the dossier, and whether to act on each is Peter's call.
i%=0 : Do While i%<2.5 : Inc i% : Loop : Print i% prints 2 instead of 3. It is gated only by #ifdef CACHE (Commands.c:5415-5450), so it is live on COM16 today, with no option set. Removing the trace cache removes it, but the cmd_do/cmd_loop hooks must go with it.argbuf and tknbuf, the probable cause of the CollTiles result, and DIM, ERASE, OPTION BASE and OPTION EXPLICIT do not invalidate it (MMtrace.c:74-77). Both go away with the cache.MMBasic.c:5160-5166). x++ lands on the second token byte, which is 0x80 or more, so the test never matches. Symbols would make it live, and programs that pass today could start reporting a clash, as the RP2350 already does.g_LocalIndex itself balances through cmd_ireturn (MM_Misc.c:1883). The effect is limited to g_current_sub_idx, profiling and the trace cache's locals.fef1f0b, today).Already answered by Peter: there are no existing build, NOCHECK or WiFi A/B figures, so 0b measures them. The reference build for the gates is chosen from the 0b results, with clocks matched through MM.INFO(CPUSPEED) and OPTION CPUSPEED nnnn. Flash timing is not pursued.
solar_eclipse (12.5 s against 8.8 s), or also from other programs? If others, where are they?