SUBs, FUNCTIONs and labels · design for review

One name index for both chips

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.

Summary

What to do, and why

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.

RP2350: about 20 KB of RAM back

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.

RP2040: fast text lookups

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.

One code path

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.

Found on the way: SUB layouts

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.

How it works today

Two chips, two designs

The parts both chips share

Where names are still looked up as text

The text path is not rare. It is taken by:

 RP2350RP2040
SUB/FUNCTION text lookupFNV-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 lookupthe 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)
Builtmemset 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 builtPrepareProgram runs at RUN, CHAIN, LOAD, LIBRARY, EDIT and before every line typed at the prompt (PicoMite.c:4571)
Limits511 SUBs, FUNCTIONs, CSUBs and labels together256 SUBs, FUNCTIONs and CSUBs; labels unlimited
Static RAM20,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.

What it costs

Measured on both boards

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, symbolsRP2040, textRP2350, symbolsRP2350, text
GOSUB, near label4.47.84.44.9
GOSUB, far label4.457-614.45.2
SUB call, 20 SUBs and FUNCTIONs in the program13.719.36.77.7
SUB call, 200 in the program—28.37.07.7
FUNCTION call, 20 in the program33.4—18.120.4
FUNCTION call, 200 in the program—55-5618.420.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.

The parameter layouts

A program calling 40 different SUBs of two parameters, once each:

BoardHeap beforeAfterTakenPer SUB
RP2040 VGA116,480105,21611,264282
PC3 (PSRAM)6,473,2166,460,92812,288307

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.

What programs use

Names in real programs

The 83 programs over 8 KB in Bas/ (games, ports, tests, libraries), counted from their source:

ProgramLinesSUBFUNCTIONLabelsLongest name
Elite port4,73220159418
Prince of Persia port (player)6,19017181217
Arkanoid2,1495731213
Thrust2,378516918
Exile physics1,58742175625
Chuckie Egg1,771338812
Recommended design

A name index sized to the program

The index

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

Around it

The parameter layouts, packed

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 heap

Once 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.

Optional: line numbers

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.

Memory

What moves

 RP2350 todayRP2350 proposedRP2040 todayRP2040 proposed
Static RAM20,48001,1280
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 called12,288 (PSRAM)~2,500 (PSRAM)11,264~2,500
Layouts, Elite-sized~66,000 (PSRAM)~16,000 (PSRAM)~66,000~16,000
Speed

What gets faster

Behaviour

What a program could notice

CaseTodayProposed
A label in both the library and the programRP2350: 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 nameno error; the first found wins, which differs by chipan error at RUN, or first wins (decision 3)
A SUB and a label of the same nameallowed; interrupts take the SUBunchanged
Order of subfun[]RP2350 program order; RP2040 alphabeticalprogram order on both: only the profiler's report order changes on the RP2040
Too many namesRP2350: 511 SUBs, FUNCTIONs and labels; RP2040: 256 SUBs and FUNCTIONsone cap for SUBs and FUNCTIONs on both chips (decision 5); labels uncapped
A name of 32 or more charactersrefused when the program is saved, on both chipsunchanged

Nothing outside MMBasic.c reads funtbl: it is not in the CSUB call table, PicoCFunctions.h, MEMORY, LIST or a saved context.

Risks

Hazards to design for

Phases

Five steps, each tested on both boards

1index

One name index for SUBs, FUNCTIONs, CSUBs and labels

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.

Gate: goldens on both boards; the lookup timings above with and without symbols; a library with SUBs and labels; CHAIN and SAVE/LOAD CONTEXT; SETTICK and SETPIN to a SUB and to a label; ON GOTO, RESTORE label, CALL, EXECUTE, MM.PROMPT and MM.END; "Duplicate name" and the too-many error; solar_eclipse.

2layouts

Packed parameter layouts

Layouts in chunks, the layout table sized to the SUBs.

Gate: the 40-SUB heap test; rbf2, rbp5d, static5; deep recursion; Elite and the Prince of Persia port on the PC3.

3subfun

subfun[] in the heap, one cap

subfun[] and the profiler's arrays sized to the program; one MAXSUBFUN cap on both chips.

Gate: as phase 1, plus OPTION PROFILING ON and a program with more SUBs than the RP2040's 256.

4heap

The RP2350's 20 KB to the heap

Whole 4 KB steps, about 4 KB left for the stack, MAX_PROG_SIZE unchanged; new magic numbers in that release.

Gate: every build's fit check; goldens; the games on the PC3.

5optional

Afterwards, if wanted

A line-number index for findline; making ON, RESTORE and CALL symbol-aware so they use the caches.

Decisions

For Peter

Decided, 2026-10-02
  1. One index for SUBs, FUNCTIONs, CSUBs and labels, with a kind byte.
  2. Labels are global: one in the library and one of the same name in the program is a duplicate, an error like any other (Peter, 2026-10-02, replacing "the current module's wins").
  3. A duplicate label is an error when the program is prepared, like a duplicate SUB name, wherever the two are.
  4. No fallback search: if there is no room for the index, the lookup says "Not enough memory".
  5. The caps stay: 512 SUBs, FUNCTIONs and CSUBs on the RP2350, 256 on the RP2040. Labels are not capped.
  6. The RP2350's freed RAM goes to the heap in phase 4, with new magic numbers in that release.
  7. The AUTOSAVE symbol collector and the parameter layouts are fixed as part of this work, which is for the b4 release.
Found on the way

From the surveys and the measurements

  1. AUTOSAVE saves a program of about 500 names as text on the RP2040, silently. AUTOSAVE takes all but about 11 KB of the heap for its buffer (FileIO.c:7074), so the symbol collector gets only its smallest block, 8 KB (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.
  2. CHAIN may leave the IF table pointing into the restored heap. CHAIN's 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).
  3. Parameter layouts take a 256-byte page each (above): 11 KB of the RP2040 VGA's heap for 40 SUBs, up to about 66 KB for a large program.
  4. A label's cache is shared by the library and the program: the first image searched wins, and later GOTOs from the other module get that answer.
  5. Duplicate labels are never reported, and the chips pick different ones.
  6. GetIntAddress accepts a CSUB as an interrupt target (FindSubFun(p, 0) takes SUB or CSUB); interrupt dispatch then treats it as a label.
  7. findlabel(NULL) in ClearRuntime does nothing: the comment "clear the label cache" is left from older code.
  8. Dead and misleading code: an unreachable length check and 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.
  9. Two comments disagree with the code: Symbols.h:210-213 says the bindings' hot block is always in SRAM (it moves to PSRAM over 4 KB), and Symbols.c:443-445 says the bindings take "3-5 KB" (Elite's take about 33 KB).