main at 1adeb0e · open issues · updated 26 September 2026

Bugs found on main, September 2026

These turned up during the interpreter performance work on development. They were found by testing on the boards and by read-only code surveys. Each one below is present on main. For each, the report gives where it is, what goes wrong, how to confirm it, and a suggested fix.

Line numbers for items 1-20 are main at cbd99fb (2026-09-25); for items 21-27, main at 1adeb0e. "Confirmed" means seen on a board; "by reading" means found in the code and not yet reproduced.

Summary

#AreaProblemEvidenceSeverityStatus
1PIO interruptsTX "was full" test reads SM0's SHIFTCTRL for every state machineby readingmediumfixed on main b7aad97
2I2C slaveclearing a slave-ready bit can wipe one the ISR sets at the same momentby readingmediumfixed on main 0227df6
3WiFiWEB SCAN without a connection never polls, so it finds nothingconfirmedmediumfixed on main bd1a965
4DO loopsthe trace cache's DO fast path truncates a fractional limitby readingmediumfixed on main 4c42f0c
5STATICa STATIC used after a nested call can attach to the wrong variableconfirmedhighfixed on main b0f448e
6OPTION LOCAL VARIABLESa value above the compile-time size overflows g_hashlistconfirmed (hard fault)mediumfixed on main d2a8fa9
7Name tablesthree probe loops can hang when their table is fullby readinghighpart 1 fixed on main 1c95cd1; part 2 not reachable in practice
8Labels (RP2350)GOTO to a label named like a SUB uses a SUB index as a pointerconfirmedmediumfixed on main 2d5307e
9FindSubFunthe type argument is ignored on RP2350 but used on RP2040confirmedlowfixed on main edc7999
10CALL, tokeniseno length checks on two 256-byte buffersconfirmed (tokenise)mediumtokenise fixed on main c4ea0e2; the rest left, see the card
11Interruptsthe IRETURN stub rti[2] has no terminatorby reading; latentlowfixed on main fc1ca32
12CountLinesp[0] where p[1] is meant, on long linesby reading; no visible effectlowfixed on main 1a999a3
13OPTION PROFILINGinterrupt SUBs and ON…GOSUB unbalance the per-SUB framesconfirmedlowfixed on main 7238252
14MM.INFO(VARCNT)stale after ERASE and after locals are freedconfirmedlowfixed on main aa98747
15CSUB headerint dims[] selected only for PICORP2350checked by readinglow?not a firmware bug; header comment and manuals corrected on main 63dea76
16ON ERROR SKIPa nested call inside an argument can leak the outer call's frameconfirmed, both boardslowheap leak fixed on main 52078f6; nested case left
17Trace cacheglobal slot indices are never invalidatedby readinglowgone: trace cache removed on main c50b6bc
18StepperRUN during a job wipes the heap under the ISRconfirmedhighfixed on main db99419
19StepperSTEPPER ESTOP leaks the buffer of the arc that is executingconfirmedlowfixed on main 2794fe3
20Stepperarcs ran at about 2.4 mm/s with F6000 requestedconfirmedmediumdeferred (Peter, 26 September)
21Program flowa FOR, DO or GOSUB line ending in : ' comment leaves the loop, or returns past the next lineconfirmed, both boardshighfixed on main e49d3ff (from development 8740953)
22MAX, MINresults clamp at the single-precision limit, ±3.4e38confirmedlowfixed on main 50c9aa0
23ABS, SGNa float of 263 or more raises "Number too large"confirmedlowfixed on main 16d0413
24Arraysinteger subscripts are cut to 32 bits before the bounds check: a(4294967297) reads a(1)confirmedmediumfixed on main 8f617d8
25Arithmeticonly +infinity from + and * is trapped; subtraction and -infinity never areconfirmedlow-INF/NaN printing fixed on main a40112b (option A)
26ConversionsNaN becomes 9223372036854775807, printed and assigned without an errorconfirmedlowfixed on main a40112b (option A)
27Operator ^an integer power runs one multiplication per unit of the exponent, with no Ctrl-Cconfirmed (timing)lowfixed on main c04dc39
28RANDOMIZE (RP2040)a bare RANDOMIZE raises "Number out of bounds" once the board has been up about 36 minutesconfirmedmediumfixed on main 1457087
29Program flowcalling a one-letter SUB alone on a line skips the next lineconfirmedhighfixed on main 9c1aeb9
30Local variablesmarkers left by freed locals were never cleared; after a deep recursion a new local failed with "Too many local variables"confirmedmediumfixed on main 8127da0

Items 1-4 are done. Suggested order for the rest: 5, 7 and 18.

Fixed on development, ported to main (b7aad97, 0227df6, bd1a965, 4c42f0c)

1 · PIO TX interrupt: wrong FIFO depth for SMs 1-3

Where: core/MM_Misc.c:10265, checkdetailinterrupts: int full = (pioinuse->sm->shiftctrl & (1 << 30)) ? 8 : 4;

  • What goes wrong: pioinuse->sm is the array, so this always reads state machine 0's SHIFTCTRL. For SMs 1-3 the "TX FIFO was full" depth depends on whether SM0's FIFO is joined, not their own. A PIO TX interrupt on SM 1-3 then fires on the wrong edge or never fires.
  • Confirm: join the TX FIFO on SM1 but not SM0, then arm a PIO TX interrupt on SM1 and fill the FIFO. The interrupt should fire when the level drops from 8.
  • Fix: pioinuse->sm[sm].shiftctrl, as on development in 791bf93.

2 · I2C slave: lost ready bit

Where: the clears are at core/MM_Misc.c:10410, 10416, 10422, 10428 (I2C_Status &= ~…). The ISRs set bits at io/I2C.c:391, 396 (and 407, 412 for I2C2).

  • What goes wrong: the main loop's &= is a load, a modify and a store. If the I2C interrupt sets the other ready bit between the load and the store, the store wipes it. A receive and a send request arriving together can lose one interrupt.
  • Fix: clear the bit with interrupts masked (save_and_disable_interrupts/restore_interrupts), as ClearI2CStatus() does on development in 791bf93.

3 · WEB SCAN without a WiFi connection finds nothing

Where: net/WiFi.c:304: if (!WIFIconnected && startupcomplete) goto flashonly; in ProcessWeb, and WEB SCAN's wait loop at net/WiFi.c:575.

  • What goes wrong: the build uses lwIP's poll mode, where scan results arrive only through cyw43_arch_poll(). Without a connection ProcessWeb skips straight to the heartbeat, so nothing polls. The only results that get through are ones processed during the next scan's start-up command.
  • Confirmed on the PC3 (HDMIWEB, no OPTION WIFI): one run listed no networks, the next listed one. Development listed them every time.
  • Fix: a flag (WebScanActive on development) set for the scan's wait. ProcessWeb polls whenever it is set, even with no connection (development a07e8f2).

4 · DO fast path truncates a fractional limit

Where: core/MMtrace.c:3497, dofast_fill: *out_limit_i = (ctype == T_INT) ? limi : (int64_t)limf;. This path is always on in the eleven trace-cache builds, even with the trace cache switched off.

  • What goes wrong: an integer variable compared with a fractional number compares against the truncated number. DO : INC i% : LOOP WHILE i% < 2.5 stops at 2; the evaluator (and every other build) runs to 3.
  • Fix: compare in float unless both sides are integers, as the evaluator does. On development the whole fast path was rewritten and moved into Commands.c (04361c0); Bas/bench/inttests/dotest.bas checks 16 condition forms against the evaluator. On main the smallest fix is to keep a float limit when the number is not an integer.

Found by reading the code

5 · STATIC after a nested call attaches to the wrong variable

Where: core/MMBasic.c:2629 sets CurrentSubFunName on every SUB/FUNCTION entry and nothing restores it. STATIC builds its backing variable's name from it at core/Commands.c:8778-8788 (SUBNAME + "\x1e" + VAR).

  • What goes wrong: in SUB A, a STATIC executed after a call to SUB B is mangled as B\x1eVAR. It can find or create B's static instead of its own, sharing or losing state. CurrentInterruptName also overrides the SUB name for every STATIC run inside an interrupt SUB, including in SUBs that the interrupt SUB calls.
  • Confirm: SUB A : B : STATIC n : INC n : PRINT n : END SUB with a SUB B that has a STATIC n of its own; call A several times and watch n.
  • Fix: save CurrentSubFunName in the frame and restore it on return. Alternatively, derive the name from subfun[index] of the current frame when the STATIC executes.

6 · OPTION LOCAL VARIABLES above the compile-time size overflows g_hashlist

Where: core/MMBasic.c:225 struct s_hash g_hashlist[MAXLOCALVARS]. maxlocalvars can be raised up to MAXVARS-32 at :6481, and each local is pushed with no bound at :5253.

  • What goes wrong: with more live locals than the compile-time MAXLOCALVARS, g_hashlist[g_hashlistpointer++] writes past the array into whatever follows in BSS.
  • Fix: size g_hashlist to MAXVARS, or cap OPTION LOCAL VARIABLES at MAXLOCALVARS, or bound the push and raise "Too many local variables".

7 · Probe loops that can hang

  • Global variables: probe_global_slot (core/MMBasic.c:4556) only checks for wrap-around when called from inside a SUB. At level 0 a global region full of live entries and tombstones loops forever.
  • Labels: hashlabels' guard if (originalhash < 0) (:3713) tests an unsigned value, so it is never true. A full funtbl loops forever at PrepareProgram.
  • SUB lookup: FindSubFun on the RP2350 probes funtbl with no bound.
  • Fix: a counted probe (at most the table size) on each loop, with an error on overflow.

8 · RP2350: GOTO a label that shares a SUB's name

Where: SUB/FUNCTION names and labels share funtbl. For a label, index is a line pointer; for a SUB it is a small subfun index. The RP2350 findlabel (core/MMBasic.c:3742) does not check which kind it found.

  • What goes wrong: GOTO/GOSUB/RESTORE naming a SUB (or a label and a SUB with the same name) returns the SUB index as a program pointer, and execution jumps to low memory. The RP2040 scans the program for real labels and is not affected.
  • Fix: accept only index >= MAXSUBFUN in findlabel, as FindSubFun accepts only < MAXSUBFUN. Either reject a label that duplicates a SUB name at PrepareProgram, or allow both and keep them apart.

9 · FindSubFun ignores its type argument on the RP2350

Where: the RP2350 FindSubFun at core/MMBasic.c:1966 never reads type. The RP2040 version at :2020 filters SUB/CSUB (0) from FUNCTION (1) and checks the suffix.

  • What goes wrong: a SUB or CSUB name used with () inside an expression is found as a function on the RP2350 but not on the RP2040, so the same program behaves differently on the two chips.
  • Fix: filter by kind on the RP2350 as the RP2040 does.

10 · Unchecked buffer lengths: CALL, and tokenise's output

  • tokenise: tokenise never checked its output position in tknbuf (256 bytes), and output can be longer than input: the 2-byte line header, the line number, an inserted LET or PRINT, and the CALC token with MM.ANSWER on every prompt statement that starts with an operator. Confirmed 26 September: a program line of 62 short assignments (about 374 bytes tokenised) restarted the RP2040 VGA board, and +1: typed 80 times at the prompt (about 1,190 bytes) hung it.
  • Fix (c4ea0e2): tknbuf stays at 256 bytes. Every write to it is checked, and a line that will not fit is refused with "Line is too long". While a program is being saved, tokenise returns 1 instead, so SaveProgramToFlash, SaveProgramToRAM and the library save close their write first (SaveProgramToFlash has interrupts off until then). A 204-character line that tokenises to 242 bytes still runs. Test: Bas/bench/tknbuf10.py.
  • Left as they are. None of these has been reported by users, so Peter scaled the fix back to tknbuf (26 September):
    • CALL and CALL() still strcat the argument text onto the name in a 256-byte temporary string.
    • Listing: LIST, SAVE, EDIT and the error display expand ? to "Print " and add a space after each command name, so a listed line can reach about 900 characters from a 255-character source. The callers' buffers, replaceAlpha (256 bytes) and str_replace (288 bytes) all hold it. LIST of a line of ?n: statements hard-faulted the PC3. With OPTION CONTINUATION LINES, the " _" breaks format_string adds can overrun LIST's 256-byte buffer on an ordinary 255-character line.
    • Before tokenising, MIN(, MAX(, LEFT$(, HEX$( and similar names are expanded as text (MIN( becomes TOPBOTTOM(I,) through str_replace's 288-byte stack buffer. A 241-character line of nested MIN( at the prompt hard-faulted the PC3 (PC=42504F54, the text "TOPB").
    • At the prompt only: EVAL saves tknbuf with strcpy, which stops at the first statement, so a long EVAL followed by another statement gives "Invalid character"; EXECUTE drops the rest of the line. Both confirmed on the PC3. Programs are not affected.

11 · IRETURN stub has no terminator

Where: core/MM_Misc.c:10213 static char rti[2] holds only the 2-byte IRETURN token. The END SUB of an interrupt SUB returns to it, and skipelement/checkend read the byte after it.

  • Effect today: none. In both maps (RP2040 VGA, RP2350 HDMIWEB) the next variable needs 4-byte alignment, so the byte after rti is linker fill and always zero. A 1-byte variable whose name sorted just after rti would have broken every interrupt SUB's return with "Unexpected text".
  • Fix (fc1ca32): static char rti[4]; the zeros are now part of the array. It takes the two bytes of fill, so RAM use is unchanged.

12 · CountLines on lines longer than 253 bytes

Where: core/MMBasic.c:3953 p += p[0] + 2;. This is reached only for lines whose skip byte is 0xFE (length unknown). The label length is in p[1].

  • Effect: none visible. A label starts its line, and a line with skip byte 0xFE is at least 250 bytes, so the wrong skip (5 bytes whatever the label's length) stays inside the line and the line count is right.
  • Fix (1a999a3): p += p[1] + 2;, as every other label skip in the source already does.

13 · OPTION PROFILING frames unbalanced by interrupts and ON…GOSUB

Where: a SUB-style interrupt (GotAnInterrupt, core/MM_Misc.c:10527) and ON…GOSUB (core/Commands.c:8625) raise g_LocalIndex without EnterLocalFrame. Their END SUB or RETURN goes through cmd_return, which calls LeaveLocalFrame (:5976).

  • Effect: the frame of the SUB that was interrupted is popped, so OPTION PROFILING's per-SUB times are charged to the wrong SUB. Only the CACHE builds that keep these frames are affected; g_LocalIndex stays balanced.
  • Confirmed on the PC3: a SUB that ran 1 s with a 50 ms SETTICK SUB firing into it was timed at 51 ms, one that ran 0.5 s after an ON GOSUB at 82 µs, and the tick SUB was missing from the report.
  • Fix (7238252): both entries call EnterLocalFrame. An interrupt SUB is also counted and timed under its own name, since the dispatch that counts SUB calls never sees it. Now 1,000,061 µs, 500,065 µs, and the tick SUB with 19 calls. Test: Bas/bench/prof13.py.

14 · MM.INFO(VARCNT) stale

Where: g_varcnt is updated only when a variable is created. erase() (core/MMBasic.c:6577) and ClearVars(level) decrement g_Globalvarcnt/g_Localvarcnt but not the total. MM.INFO(VARCNT) is at core/MM_Misc.c:9358.

  • Confirmed on the PC3: erasing one array, then two LOCALs, then returning from the SUB changed the count by 0, 1 and 1 where the manual's "number of variables in use" needs -1, 2 and 0.
  • Fix (aa98747): MM.INFO(VARCNT) returns g_Globalvarcnt + g_Localvarcnt. Test: Bas/bench/varcnt14.py, passes on the PC3 and the VGA.

15 · CSUB header: int dims[] only for PICORP2350 (to verify)

Where: PicoCFunctions.h:400 selects int dims[MAXDIM] only #ifdef PICORP2350, else short. Every RP2350 firmware build uses int dimtbl[5], and the variable-table layout is part of the CSUB ABI (CallTable 0x60).

  • Check: how CSUBs are compiled for the other RP2350 targets (HDMI, HDMIUSB, VGARP2350 …). If PICORP2350 is not defined for them, a CSUB that walks g_vartbl reads the wrong offsets.
  • Checked: not a firmware bug. In the header PICORP2350 is the CSUB author's chip select, not the firmware variant: with it defined the header gives MAXDIM 5 and int dims[], as every RP2350 build has (rp2350 comes from IS_RP2350), and without it 6 and short, as every RP2040 build has. The other fields line up byte for byte.
  • Corrected (63dea76): the switch's comment said it was "kept only for any chip-specific user code", which invites leaving it defined for an RP2040. It now says that it sets the variable-table layout and must match the chip.
  • Manuals qualified: armcfgen (the CSUB manual), the mmb2csub manual and the V6.04.00 release notes said one compiled CSUB runs unchanged on both chips and every variant. That holds only for a CSUB that reads neither g_vartbl (dims[]: 5 × int on the RP2350, 6 × short on the RP2040) nor Option (laid out per variant). armcfgen §1.5 now says so, with how to build one that does; mmb2csub notes that its generated code reads neither, so its claim stands. The user manual makes no such claim.

16 · Leak when a nested call's argument errors under ON ERROR SKIP

Where: DefinedSubFunMem (core/MMBasic.c:536) is one global sentinel. It is set during argument processing (:2387) and cleared on success (:2628). In Foo(Bar(x), bad) the inner call clears it. If the outer call's later argument then errors under OPTION ERROR SKIP, the error handler does not unwind the outer frame.

  • Wider than it read. The gosubstack holds every SUB and FUNCTION call, not only GOSUBs. And the error cleanup undid the call's frame but freed nothing, so even without a nested call, every skipped error in a call's arguments leaked: on the RP2040 the 2.8 KB argument block (50 errors in Foo(1/z, 1) used up the VGA's heap, 100,608 bytes, after which each call leaked its frame as well), and on both chips the 256-byte copy of each string argument (13,312 bytes over 50 on the PC3).
  • Fixed (52078f6): the cleanup frees the string copies made so far and the argument block; the prompt's landing clears the sentinel so a CTRL-C cannot leave a stale block. Test: Bas/bench/argleak16.py, heap unchanged on both boards.
  • Left as an edge case (Peter, 26 September): a nested call in the argument list. Foo(Bar(1), 1/z) - Bar's success clears the one sentinel - and Foo(Bar(1/z)) - the cleanup undoes one level - still leak the outer call's frame (and on the RP2040 its argument block). After 50 such skipped errors no SUB or FUNCTION can be called ("Too many nested SUB/FUN"; seen on the PC3 with a 45-deep call). The full fix would keep a per-call record on the C stack (about 25 lines, no RAM).

17 · Trace cache keeps global indices it never invalidates

Where: core/MMtrace.c. Its LET/INC ops store global slot indices and raw pointers. TraceCacheInvalidateAll (:3287) is called only by the trace cache's own option commands, although the file's comments say DIM, ERASE, REDIM and CLEAR invalidate it.

  • Effect (only with OPTION TRACECACHE ON): after ERASE or REDIM, a cached LET can write into a slot now used by another variable. Status: the trace cache was removed on development (04361c0).

Stepper (RP2350)

18 · RUN during a stepper job wipes the heap under the ISR

  • What goes wrong: RUN clears and reinitialises the heap while the stepper timer interrupt may still be walking planner blocks and arc buffers that live in it.
  • Confirmed on the PC3 (audio disabled, one axis on GP10/GP22, no motor): after a program queued 80 s of moves and ended, the next RUN found the stepper still active with the job frozen on the wiped buffer. Also: a re-run program could not STEPPER INIT ("Stepper already initialized"), and the STEPPER CLOSE used to get round that (Testfiles/FishFeeder.bas) freed the stale buffer pointer. The Stepper Reference says nothing about RUN or END.
  • Fix (db99419): InitHeap calls stepper_close_subsystem() before the wipe (Peter chose this over stopping it on RUN/NEW/LOAD only), so every heap wipe - RUN, NEW, LOAD, CHAIN, and FILES / EDIT FILE / RAM FILE LOAD from inside a program - ends a stepper job and a program must STEPPER INIT again. Test: Bas/bench/steprun18.py.
  • Noted, not changed: CTRL-C does not stop motion; do_end aborts the stepper only after an error, so a queued job carries on after CTRL-C.

19 · STEPPER ESTOP leaks the executing arc's buffer

  • What goes wrong: ESTOP drops the queue, but the buffer of the arc being executed is not handed to the retired list; the next block the ISR loads overwrites the pointer. Only the STEPPER ESTOP command path missed it: the hardware E-STOP and limit trips, the error abort and STEPPER CLOSE release it.
  • Confirmed and fixed (2794fe3): the ESTOP path calls stepper_release_current_arc(). A 157 mm arc stopped after 1 s lost 3,584 bytes until the next RUN; now 0. Test: Bas/bench/estoparc19.py (PC3 with audio disabled, X on GP2/GP3, Y on GP4/GP5).

20 · Arcs slower than the requested feed rate

  • What goes wrong: on the PC3 test rig, arcs ran at about 2.4 mm/s with F6000 requested; straight moves were not affected. Next step: check the arc segment feed-rate clamp and the per-segment junction speed against the planner.
  • Deferred (Peter, 26 September: not today's issue). One unexamined data point: on the PC3 (audio disabled, X on GP2/GP3, Y on GP4/GP5, 50 mm/s, 200 mm/s²), a program that timed a 157 mm straight move and then a 157 mm semicircle, both at F6000, had not finished after 280 s. The stepper was still active when it was stopped.

Found on 26 September

These turned up while designing Route B on development, from four code surveys and board checks on the PC3 and the RP2040 VGA. Line numbers are for main at 1adeb0e. "Confirmed" means seen on a board running a development build whose code in that area is the same as main's.

21 · A loop whose first line ends in : ' comment runs once

Where: the comment fast path in ExecuteProgram, core/MMBasic.c:1871. It jumps to the line after line_start, the last T_NEWLINE the loop crossed. NEXT, LOOP, RETURN and IRETURN jump back into the middle of an earlier line without crossing its T_NEWLINE, so a comment element they land on is taken to belong to the wrong line.

  • Effect: each of these is broken.
    • FOR i = 1 TO 3 : ' c runs its body once.
    • DO : ' d … LOOP UNTIL k = 2 leaves after one pass.
    • GOSUB x : ' c returns to the line after the RETURN's line.
    Without the colon (FOR i = 1 TO 3 ' c) the comment is part of the FOR statement, and nothing goes wrong. The code is in the repository's first commit, so every release since 6.03.00 has it.
  • Confirm: Bas/bench/cmtloop.py PORT fails on both boards before the fix and passes after it.
  • Fix: also require p > line_start && p < line_start + line_skip (development 8740953; on main e49d3ff). The same shortcut in GetNextCommand (:7194) is safe, because its line_start is local to one forward scan.

22 · MAX and MIN clamp at the single-precision limit

Where: fun_max_min, core/Functions.c:1855-1890, starts from -FLT_MAX or FLT_MAX.

  • Effect: MAX(-1e300, -2e300) gives -3.402823466e+38, and MIN(1e300, 2e300) gives 3.402823466e+38. Fix: start from the first argument, or from -DBL_MAX/DBL_MAX.
  • Fixed (50c9aa0): MAX and MIN start from their first argument. Test: Bas/bench/maxsub22.py (PC3 and PICOMIN).

23 · ABS and SGN reject floats of 263 or more

Where: fun_abs and fun_sgn (core/Functions.c:555, 1232) evaluate their argument with an integer target type, so a float argument goes through FloatToInt64.

  • Effect: ABS(1e19) raises "Number too large". Fix: evaluate with T_NOTYPE and branch on the type that comes back.
  • More detail (26 September): both chips; any float of magnitude 263 or more fails (ABS(-1e19), SGN(1e300)), while ABS(-1.5) and ABS(1e18) work. evaluate with an integer target always runs FloatToInt64 on a float result but still returns the float, which ABS and SGN then use, so the conversion's only effect is the error. No other function does this. Fix: evaluate with T_NOTYPE (two words). Awaiting Peter.
  • Fixed (16d0413): ABS and SGN evaluate with no target type and refuse a string themselves. Test: Bas/bench/numedge23.py, PC3 and VGA.

24 · Integer subscripts are cut to 32 bits before the bounds check

Where: the subscript loops in findvar (core/MMBasic.c:4834) and the struct-member paths (:4220, 4344). A float subscript goes through FloatToInt32, but an integer one is stored with dim[i] = in, which truncates the 64-bit value.

  • Effect: with DIM a(3), a(4294967297) reads a(1) without an error. A runaway index in a user's program can therefore hit the wrong element silently. Fix: range-check in against INT_MIN/INT_MAX before the assignment and raise "Index out of bounds".
  • Fixed (8f617d8): Peter: no array can be that big, so it should just error. An integer subscript that does not fit in 32 bits is now "Index out of bounds" at all three sites (before, a(2^31) wrapped negative and said "Dimensions"). Test: Bas/bench/maxsub22.py.

25 · Only +infinity is trapped, and subtraction never is

Where: core/Operators.c. op_add, op_mul, op_div and op_exp test fret == INFINITY; op_subtract has no test, and nothing tests -INFINITY.

  • Effect: 1e308 + 1e308 raises "Overflow", but 1e308 - (-1e308) prints INF (both seen on the PC3). By reading, no operation traps -INF. Fix: test isinf(fret) in all five, if raising an error is the intended behaviour.
  • More detail (26 September): functions do not check either (EXP(1000) prints INF). Printing -infinity is broken: FloatToStr special-cases only +INF, so for -INF it takes (int) of an infinite exponent, divides -inf by inf and recurses with NaN: garbage on the RP2350 (9223372036854775807e+2147483647) and a hang on the RP2040 VGA (?-1e308-1e308 needed a hardware reset). Options put to Peter: (A) make printing and FloatToInt64 safe for -INF and NaN; (B) also trap isinf() in all five operators. Awaiting Peter.
  • Fixed, option A (a40112b): FloatToStr prints -INF and NAN. Arithmetic still produces INF and NaN (option B, trapping in the operators, not taken).

26 · NaN becomes 9223372036854775807

Where: FloatToInt64 (core/MMBasic.c:6789). Its range test is false for NaN, so NaN reaches the C conversion. Exactly 263 also passes the test, because the bound rounds to 263, and then converts with undefined behaviour.

  • Effect: with x infinite (item 25), y = x - x prints 9223372036854775807, and z% = y stores it. Fix: reject NaN, and use >= 9223372036854775808.0 as the upper bound.
  • More detail (26 September): INF-INF prints as 9223372036854775807, and INT, FIX, CINT and assignment to an integer all give 9223372036854775807 without an error (PC3). Goes with item 25 option A. Awaiting Peter.
  • Fixed, option A (a40112b): FloatToInt64 and FloatToInt32 (and their inline copies in MMBasic.h, which the other files use) refuse NaN, and FloatToInt64 refuses 263 itself. INT and FIX, which used plain casts, now go through FloatToInt64 as well, so INT(1e30) and FIX(-1e30) raise "Number too large" too. On the RP2040 INF-INF is INF (its soft-float library). Test: Bas/bench/numedge23.py.

27 · A large integer exponent runs one loop step per unit

Where: op_exp's integer branch: for (iret = i = 1; i <= iarg2; i++) iret *= iarg1.

  • Effect: 1 ^ 30000000 took 800 ms on the PC3, so 1 ^ 4000000000 would lock the board for about two minutes. Ctrl-C cannot stop it, because the loop never returns to the interpreter. Fix: exponentiation by squaring, which is at most 64 steps and gives the same wrapped result.
  • More detail (26 September): 1^30000000 takes 1.04 s on the PC3 and 1^300000000 10.4 s; a CTRL-C after 1 s was acted on only when the loop ended. Fix: exponentiation by squaring (about 8 lines, same wrapped results, faster above an exponent of about 4). Awaiting Peter.
  • Fixed (c04dc39): exponentiation by squaring in unsigned arithmetic: at most 64 steps, same wrapped results; 1^4000000000 under 100 ms.

28 · A bare RANDOMIZE fails after about 36 minutes (RP2040)

Where: cmd_randomize, core/Commands.c, the #ifndef rp2350 version: i = time_us_32(); into an int, then if (i < 0) StandardError(21);.

  • Effect: the microsecond counter passes 231 after 35.8 minutes of uptime, so the seed turns negative and a bare RANDOMIZE reports "Number out of bounds". It works again for the next 35.8 minutes after the counter wraps at 71.6 minutes, and so on. Found by the goldens corpus (mathr.bas, line 58) on the RP2040 VGA on 26 September, and reproduced at the prompt. The RP2350 has its own RANDOMIZE and is not affected. Fix: take the time seed as time_us_32() & 0x7FFFFFFF, so only a user-supplied negative seed is refused.
  • Fixed (1457087): the time seed is masked with 0x7FFFFFFF; a negative seed the user gives is still refused. Checked on the PICOMIN.

29 · Calling a one-letter SUB alone on a line skips the next line

Where: the pre-scan in ExecuteProgram, core/MMBasic.c:1873: nextstmt = cmdline = p + sizeof(CommandToken); runs for every statement that is not a comment, including a call to a user SUB, which starts with the name rather than a 2-byte command token.

  • Effect: for a one-letter SUB called with no arguments and nothing after it on the line, the statement is the letter and its terminating 0. Two bytes on is the next line's header, so skipelement runs to the end of the next line. DefinedSubFun pushes that as the return address, and the line after the call is skipped silently.
  • Seen on the PC3 on 26 September, while testing item 5:
    • SUB Q ... Q followed by PRINT "next line" never prints.
    • These are unaffected: a two-letter name, Q 1, Q : PRINT ..., and CALL "Q".
    The code is in the repository's first commit, and probably upstream since commands became 2-byte tokens.
  • Fix: add sizeof(CommandToken) only when p[0] and p[1] are both command-token bytes; for a SUB call start at p.

30 · Markers left by freed locals filled the local variable table

Where: ClearVars(level), core/MMBasic.c. A returning frame's locals are freed newest first; a freed slot whose next slot is in use becomes a marker (T_BLOCKED), so that a search for a variable further along the run does not stop early. Nothing cleared a marker once it was no longer needed.

  • Effect: unwinding a deep recursion frees every local while its older neighbour is still live, so the whole run is left as markers. New locals reuse the marker nearest the end of their search path, the empty slots get used up, and a search that finds none goes all the way round: "Too many local variables", after ClearVars(0) has wiped every variable, although most of the region is free. Found on 26 September while testing item 6: with OPTION LOCAL VARIABLES 400, a recursion holding 350 live locals failed on its second pass. Short names spread badly with that modulus (19 slots apart, against 147 at 256), which made it worse. At the default split, 250 of 256 survived repeated passes.
  • Fix (8127da0): when the slot after the one being freed is empty, ClearVars empties it and any run of markers just before it. After a full unwind the region is clean. Test: Bas/bench/locvars6.py.
  • Item 7 part 2 is the same mechanism for globals: erase() leaves markers the same way. Peter judged it not reachable in practice (erased globals are reused and rare), so it is not changed.

Also found, but not on main: CHAIN and SAVE/LOAD CONTEXT left stale symbol bindings on development (fixed there in 8d3ee25). Symbols exist only on development.

Already fixed on main in this period

Not a bug: recursion from inside a FOR/NEXT or DO/LOOP corrupts the loop, which the manual documents (Recursion, page 41).