The RP2350 finds a SUB, FUNCTION or label through an 18 KB static hash table that stores a copy of every name. The RP2040 cannot afford that table, so it sorts its SUB list and binary-searches it, and finds a label by reading the program line by line. This review proposes one structure for both chips, built the way the variable table now is: a small index sized to the program, made in the heap, holding no copies of names. It frees about 20 KB of static RAM on every RP2350 build, makes the RP2040's text lookups as fast as the RP2350's, and fixes a heap cost found on the way that can take 10-60 KB of an RP2040's heap.
Branch development at 0036a7e (V7.0.00b3) · evidence: three read-only code surveys (SUB/FUNCTION lookup, labels and line numbers, the symbol layer and Route B), a count of the names in the 83 programs over 8 KB in Bas/, and timings on the RP2040 VGA (COM20, 315 MHz) and the PC3 (HDMIWEB, 315 MHz) on 2026-10-02. No code has been changed.
Replace the RP2350's funtbl and the RP2040's sorted list and label scan with one index of 8-byte slots, built in the heap when a program is prepared, sized to its names. A slot holds a 16-bit hash, the name's length, its kind and its target; the name itself is read from the program when a hash matches. The symbol layer's per-name caches stay as the fast path in front of it.
funtbl (18,432 bytes) and subfun[] (2,048 bytes) are static on every RP2350 build whether a program has two SUBs or five hundred. The index costs 128 bytes for a small program and 4 KB for Elite, from the heap. The 20 KB goes to the heap as the variable table's did.
Without symbols, a SUB call costs 6-15 µs more on the RP2040 than with them, growing with the number of SUBs, and a label 300 lines away costs 57 µs against 4 µs. A hash makes both constant: the RP2350 shows the text path within about 1 µs of the symbol path at any size.
Two versions of FindSubFunText, two of findlabel_text, a bubble sort and a first-letter index on the RP2040, hashlabels on the RP2350: all go, and both chips find names, order subfun[] and resolve clashes the same way.
Each SUB's parameter layout, read on its first call, takes at least a 256-byte heap page. 40 SUBs called once took 11 KB of the RP2040 VGA's heap; an Elite-sized program would take 66 KB. Packing the layouts into chunks cuts that to a few KB.
Five phases, each tested on the RP2040 VGA and the PC3. Seven decisions are needed (see Decisions), and the review found nine other problems along the way (see Found on the way), two of which matter for speed today.
subfun[MAXSUBFUN] (MMBasic.c:136): a pointer to each SUB, FUNCTION and CSUB definition, library first then program, built by PrepareProgramExt. MAXSUBFUN is 512 on the RP2350 and 256 on the RP2040. Route B, the parameter layouts, the profiler and interrupt dispatch all use its index or its pointers.PrepareProgram:
SymS[k]: the subfun[] index for name k (or -1, cached too). Every SUB call, FUNCTION call and array reference whose name is a symbol goes through it (FindSubFun, MMBasic.c:2494-2515).SymCold[k].labind: the line of label k, used by GOTO, GOSUB and IF ... GOTO when the operand is a symbol (findlabel, MMBasic.c:4526-4536).SubLay, MMBasic.c:2668-2801): a SUB's parameter list, read on its first call and kept, one heap allocation per SUB.The text path is not rare. It is taken by:
OPTION SYMBOLS OFF, saved by older firmware, or saved as text because its names did not fit the symbol collector (see Found on the way: on the RP2040 this happens to an AUTOSAVEd program at about 500 names);ON x GOTO/GOSUB, RESTORE label, CALL "name" and CALL(), on every execution;EXECUTE, MM.PROMPT (every prompt line), MM.END, MM.STARTUP, and the user display driver's MM.USER_RECTANGLE and MM.USER_BITMAP (on every drawing call);SETTICK, SETPIN, ON KEY and about thirty others), once per setup.| RP2350 | RP2040 | |
|---|---|---|
| SUB/FUNCTION text lookup | FNV-1a hash of the upper-cased name, % 512, linear probe in funtbl, comparing against the name stored there (MMBasic.c:2353-2413) | count subfun[] to its NULL on every call, then a binary search within the first letter's range, comparing against the name in the program (2415-2490) |
| Label text lookup | the same funtbl: labels share it with SUBs, told apart by index ≥ MAXSUBFUN (4346-4410) | read the program line by line from the start of the current module, comparing every T_LABEL (4413-4522) |
Line numbers (findline) | a walk from the start of the module on every call, on both chips, with no cache (4149-4254) | |
| Built | memset of 18 KB and a rehash of every name and label at every PrepareProgram (1092-1151, hashlabels 4257-4340) | a bubble sort of subfun[] and a first-letter index at every PrepareProgram (1042-1088) |
| How often built | PrepareProgram runs at RUN, CHAIN, LOAD, LIBRARY, EDIT and before every line typed at the prompt (PicoMite.c:4571) | |
| Limits | 511 SUBs, FUNCTIONs, CSUBs and labels together | 256 SUBs, FUNCTIONs and CSUBs; labels unlimited |
| Static RAM | 20,480 bytes (funtbl + subfun) | 1,128 bytes (subfun + the letter index) |
FindSubFun is in RAM on every build except the RP2040 WebMite, whose RAM is too short; the code is the same.
A test program timed GOSUB to a label just after the loops ("near") and to one 300 lines further on ("far"), and calls to the first and last of 100 SUBs and 100 FUNCTIONs whose names begin with the same letter. A second version had 10 of each. Microseconds per call, empty loop subtracted, both boards at 315 MHz.
| RP2040, symbols | RP2040, text | RP2350, symbols | RP2350, text | |
|---|---|---|---|---|
| GOSUB, near label | 4.4 | 7.8 | 4.4 | 4.9 |
| GOSUB, far label | 4.4 | 57-61 | 4.4 | 5.2 |
| SUB call, 20 SUBs and FUNCTIONs in the program | 13.7 | 19.3 | 6.7 | 7.7 |
| SUB call, 200 in the program | — | 28.3 | 7.0 | 7.7 |
| FUNCTION call, 20 in the program | 33.4 | — | 18.1 | 20.4 |
| FUNCTION call, 200 in the program | — | 55-56 | 18.4 | 20.8 |
The first and the hundredth SUB cost the same on both chips: the RP2040's cost grows with the number of SUBs (the count of subfun[] on every call), not with a SUB's place in the list. The RP2040 cannot give a symbols figure for the 200-SUB program: it saved that program as text (3 symbol bytes in its image against 307 in the 20-SUB one), which is itself a finding (Found on the way, item 1). On the RP2350 the hash keeps the text path within about a microsecond of the symbol path at any size.
A program calling 40 different SUBs of two parameters, once each:
| Board | Heap before | After | Taken | Per SUB |
|---|---|---|---|---|
| RP2040 VGA | 116,480 | 105,216 | 11,264 | 282 |
| PC3 (PSRAM) | 6,473,216 | 6,460,928 | 12,288 | 307 |
A layout is 12 bytes, 16 per parameter and the parameter names, typically 40-100 bytes, but each is its own allocation and the heap gives whole 256-byte pages; the table of layouts adds 1 KB (RP2040) or 2 KB (RP2350) on the first call. On the PC3 it comes from PSRAM and does not matter. On an RP2040 it comes out of the program's own heap: a program calling 100 SUBs loses about 27 KB of the VGA's 120 KB, and Elite or the Prince of Persia port, called in full, about 66 KB. This arrived with V7 (P6 F1), so it can stop a 6.04 program that fitted.
The 83 programs over 8 KB in Bas/ (games, ports, tests, libraries), counted from their source:
| Program | Lines | SUB | FUNCTION | Labels | Longest name |
|---|---|---|---|---|---|
| Elite port | 4,732 | 201 | 59 | 4 | 18 |
| Prince of Persia port (player) | 6,190 | 171 | 81 | 2 | 17 |
| Arkanoid | 2,149 | 57 | 3 | 12 | 13 |
| Thrust | 2,378 | 51 | 6 | 9 | 18 |
| Exile physics | 1,587 | 42 | 17 | 56 | 25 |
| Chuckie Egg | 1,771 | 33 | 8 | 8 | 12 |
funtbl keeps 32 bytes for each.struct s_nameslot { // 8 bytes; no name stored
uint16_t hash16; // the top 16 bits of the FNV-1a hash of the upper-cased name
uint8_t len; // its length (1-31)
uint8_t kind; // 0 empty, SUB (and CSUB), FUNCTION, LABEL; a library bit
uint32_t target; // a subfun[] index, or the T_NEWLINE of the label's line
};
static struct s_nameslot *NameIndex; // in the heap, a power of two of slots
static int NameMask; // slots - 1
PrepareProgram, after PrepareProgramExt has made subfun[]: one pass over subfun[] and one over the line headers for T_LABEL, library first, as today.findvar already does, probing while slots are full, and accepting a slot whose hash16, length and kind match and whose name, read through NameView at the definition (after the SUB token, or the T_LABEL text), is the same ignoring case. The comparison runs only on a hash match: the variable-table review found length plus hash16 separated every name in six programs.index < MAXSUBFUN trick that tells a SUB from a label today, so a label and a SUB of the same name stay possible, as now.GetSRAMMemory), 128 bytes to 4 KB. At RUN it is made just after the heap is wiped, so it sits at the top with the variables' first chunks.FindSubFunText and one findlabel_text, on both chips, both a probe of the index. The RP2040's sort, letter index, binary search and line scan go, and with them its per-call count of subfun[].subfun[] keeps program order on both chips (library, then program). The RP2040 sorted it only for the binary search. Route B already depends on this order on the RP2350.SymS and labind remain the fast path, filled from the index on first use instead of from funtbl or a scan.InitHeap, beside SymBindForget and VarChunksForget) and rebuilt by the next PrepareProgram. At the prompt, where PrepareProgram(false) runs before every line, it is rebuilt only when the program or library image has changed (a stamp of the two image pointers and their save counts), so typing commands does not churn the heap.RestoreContext puts back the old heap, the index is forgotten and rebuilt for the current program, as the symbol bindings are (Commands.c:4543-4544). See Found on the way, item 2, for the IF table, which needs the same.PrepareProgram(false) leaves it absent and the text lookups say so with "Not enough memory", like any allocation. A program being RUN always has an empty heap to make it in.The layouts move into the same kind of chunk the variables use: a few 1 KB blocks taken from the heap (the PSRAM on the RP2350, as now) and filled from the bottom, all freed together where the layouts are freed today (SymBindFree). The table of layout pointers is sized to the number of SUBs rather than MAXSUBFUN. 40 two-parameter SUBs then take about 2.5 KB rather than 11 KB on the RP2040.
subfun[] in the heapOnce nothing static is sized by it, subfun[] itself can be made in the heap with the index, exactly as long as the program needs, and MAXSUBFUN becomes a cap rather than a reservation. Its other users (the profiler's three arrays and the layout table) can be sized by the count; the width of the index field in Route B's call records must be checked against the new cap. The cap can then be the same on both chips, high enough for Elite's 260.
findline walks the program on both chips for every GOTO 100, GOSUB, RESTORE and IF ... THEN 100. A program that uses line numbers could have them indexed the same way (a slot per numbered line, made only when the program has numbered lines). Old-style programs would gain most; few V7 programs use them. Left as an option.
| RP2350 today | RP2350 proposed | RP2040 today | RP2040 proposed | |
|---|---|---|---|---|
| Static RAM | 20,480 | 0 | 1,128 | 0 |
| Index, 30 names (typical) | — | 512 | — | 512 |
| Index, 264 names (Elite) | — | 4,096 | — | 4,096 |
subfun[] in the heap | — | 4 per SUB | — | 4 per SUB |
| Layouts, 40 SUBs called | 12,288 (PSRAM) | ~2,500 (PSRAM) | 11,264 | ~2,500 |
| Layouts, Elite-sized | ~66,000 (PSRAM) | ~16,000 (PSRAM) | ~66,000 | ~16,000 |
subfun[] did.ON x GOTO, RESTORE label, CALL, MM.PROMPT and the user display driver's calls.PrepareProgram no longer clears 18 KB and rehashes every name before each line typed at the prompt.| Case | Today | Proposed |
|---|---|---|
| A label in both the library and the program | RP2350: the library's always; RP2040: the current module's first. A symbol's cache then keeps whichever was found first, for both modules. | One rule on both chips (decision 2) |
| Two labels of the same name | no error; the first found wins, which differs by chip | an error at RUN, or first wins (decision 3) |
| A SUB and a label of the same name | allowed; interrupts take the SUB | unchanged |
Order of subfun[] | RP2350 program order; RP2040 alphabetical | program order on both: only the profiler's report order changes on the RP2040 |
| Too many names | RP2350: 511 SUBs, FUNCTIONs and labels; RP2040: 256 SUBs and FUNCTIONs | one cap for SUBs and FUNCTIONs on both chips (decision 5); labels uncapped |
| A name of 32 or more characters | refused when the program is saved, on both chips | unchanged |
Nothing outside MMBasic.c reads funtbl: it is not in the CSUB call table, PicoCFunctions.h, MEMORY, LIST or a saved context.
RestoreContext puts the old heap back over it. Anything PrepareProgram made in the heap must be forgotten and remade after the restore. The symbol bindings do this; the IF table appears not to (Found on the way, 2).subfun[] while the program keeps running (FileIO.c:861-879 documents it). Today the RP2350's stale funtbl would point the lookup at a NULL definition; the fix there (PrepareProgram(true)) must stay.subfun[i] when the interrupt is set up, not a pointer into subfun[]; moving subfun[] into the heap does not affect them.subfun[] indices in persisted streams and reuses a stream when the images' CRCs match; the order must stay library-then-program, which it already is on the RP2350.FindSubFunText, findlabel_text, hashlabels, the funtbl build in PrepareProgram, findvar's SUB-name clash check (which probes funtbl on the RP2350 and scans subfun[] on the RP2040: both become an index probe), and GetIntAddress. All in MMBasic.c except GetIntAddress.The index in the heap; one FindSubFunText and one findlabel_text; findvar's clash check on the index; subfun[] in program order on both chips; funtbl, hashlabels, the RP2040 sort, letter index and label scan removed; rebuilt after RestoreContext.
Layouts in chunks, the layout table sized to the SUBs.
subfun[] in the heap, one capsubfun[] and the profiler's arrays sized to the program; one MAXSUBFUN cap on both chips.
Whole 4 KB steps, about 4 KB left for the stack, MAX_PROG_SIZE unchanged; new magic numbers in that release.
A line-number index for findline; making ON, RESTORE and CALL symbol-aware so they use the caches.
SymBegin, Symbols.c:731-760). At about 500 names that block overflows and the save falls back to text, without trying the borrowed space after the source that LOAD uses. Such a program then runs every name lookup as text: the 821-line test program above did. Fix: on overflow of an owned block, retry with the borrowed space, or let AUTOSAVE leave the collector room.PrepareProgram(true) builds the IF/ENDIF table in the fresh heap, then RestoreContext restores the old heap over it, and nothing rebuilds it (inferred from the code, not reproduced).GetIntAddress accepts a CSUB as an interrupt target (FindSubFun(p, 0) takes SUB or CSUB); interrupt dispatch then treats it as a label.findlabel(NULL) in ClearRuntime does nothing: the comment "clear the label cache" is left from older code.return in the RP2350 funtbl build and findlabel_text; a suffix test in the RP2040 FindSubFunText that is always true; findline's header says it returns the line start but it returns the line-number byte.