From 17af2d406c22f5313e0fd268e2bd8613e175b682 Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Mon, 3 Aug 2026 21:44:16 -0400 Subject: [PATCH 1/3] Cut akbasic_Runtime's static footprint from 10.75 MiB to 2.40 MiB Nothing in this interpreter mallocs; every pool is a fixed array sized by an AKBASIC_MAX_* constant, so sizeof(akbasic_Runtime) is a compile-time number and most of it was headroom nobody was using. Measured concurrent-use high-water marks off examples/breakout and examples/megademo -- the two most demanding programs this interpreter runs -- against each pool's ceiling: AKBASIC_MAX_ENVIRONMENTS 32 -> 12 (measured peak concurrency: 6-7) AKBASIC_MAX_FUNCTIONS 64 -> 8 (measured: 0, neither program uses DEF FN) AKBASIC_MAX_ARRAY_VALUES 4096 -> 2048 (measured peak: 1618 slots) AKBASIC_MAX_SOURCE_LINES 9999 -> 2048 (measured: ~1270-1496 non-blank lines) AKBASIC_SYMTAB_MAX_SLOTS 256 -> 172 (no caller ever requests more than 128) AKBASIC_SYMTAB_MAX_KEY 64 -> 24 (longest identifier measured: 11 chars) AKBASIC_MAX_LINE_LENGTH 256 -> 80 (Commodore BASIC's own line limit) AKBASIC_MAX_VARIABLES (128) is untouched on purpose: breakout alone reaches 121 of 128 concurrent named variables, so it has the least slack of any pool measured and is not a shrink candidate. akbasic_Variable.name shrinks from AKBASIC_MAX_STRING_LENGTH (256) to AKBASIC_SYMTAB_MAX_KEY: every variable name is registered with akbasic_symtab_set() right after this field is populated (akbasic_environment_create(), src/environment.c), and that call already refuses anything AKBASIC_SYMTAB_MAX_KEY characters or longer. The wider field was headroom nothing could ever put a byte into. Two defects surfaced while testing the line-length drop against the golden corpus, both fixed here because the 80-byte ceiling makes them routine rather than theoretical: - sourcepath (runtime.h) was borrowing AKBASIC_MAX_LINE_LENGTH by accident. It holds a directory, not a line of BASIC, and this checkout's own test paths are 81+ characters deep -- every golden test failed to load until this split into its own AKBASIC_MAX_SOURCE_PATH_LENGTH, backed by PATH_MAX the way libakerror already sizes its own path buffers. - src/sink_stdio.c's stdio_readline() called aksl_fgets() but never checked its own documented contract: a full buffer with no trailing newline means the line was longer than the buffer, and the unread remainder is still in the stream. Unchecked, the next readline() picks that remainder up as its own statement -- a real line silently becomes two wrong ones instead of a clean AKBASIC_ERR_BOUNDS refusal. At 256 bytes this was theoretical; at 80 it is not, so it now refuses loudly. tests/value_pool.c's test_pool_is_untouched_by_scopes() was pinned to the old 4x1024=4096 pool math (four max-size arrays proving nothing leaked); rewritten to 2x1024=2048 for the same proof against the new AKBASIC_MAX_ARRAY_VALUES. Known consequence, tracked in andrew/akbasic#32 rather than worked around here: two files in the protected tests/reference/ corpus (language/functions/mod.bas, language/flowcontrol/nestedforloopwaitingfor command.bas) have 82-character lines and cannot be shortened -- MAINTENANCE.md and CMakeLists.txt:585 are explicit that tests/reference/ is never edited to suit this interpreter. Twelve tests/language/ cases and one docs/18 line are in the same position but are this project's own content. Shipping 80 anyway, with the fallout tracked rather than hidden, was an explicit call on this PR rather than something decided here. Verified: cmake --build build-akgl && ctest --test-dir build-akgl, 97/112 (15 known failures, all AKBASIC_MAX_LINE_LENGTH-related, filed as #32). Co-Authored-By: Claude Sonnet 5 Co-Authored-By: Andrew Kesterson --- include/akbasic/runtime.h | 8 ++++++-- include/akbasic/symtab.h | 12 ++++++++++-- include/akbasic/types.h | 37 +++++++++++++++++++++++++++++++------ include/akbasic/variable.h | 12 +++++++++++- src/runtime.c | 2 +- src/sink_stdio.c | 13 +++++++++++++ tests/value_pool.c | 10 ++++------ 7 files changed, 76 insertions(+), 18 deletions(-) diff --git a/include/akbasic/runtime.h b/include/akbasic/runtime.h index f828d69..8de41fa 100644 --- a/include/akbasic/runtime.h +++ b/include/akbasic/runtime.h @@ -211,8 +211,12 @@ typedef struct akbasic_Runtime * not exist relative to the working directory, which is what makes a `.bas` * beside its art work from anywhere. Group F's disk verbs will want the * same, which is why it is on the runtime rather than in the sprite state. + * + * Sized to AKBASIC_MAX_SOURCE_PATH_LENGTH, not AKBASIC_MAX_LINE_LENGTH: a + * directory is a filesystem path, not a line of BASIC, and the two do not + * belong to the same budget. */ - char sourcepath[AKBASIC_MAX_LINE_LENGTH]; + char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH]; /* * The armed interrupts, and the environment the one currently running was @@ -365,7 +369,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj, * @param path Path to the program file, or NULL for none. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When `obj` is NULL. - * @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH. + * @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path); diff --git a/include/akbasic/symtab.h b/include/akbasic/symtab.h index c636cf3..63af692 100644 --- a/include/akbasic/symtab.h +++ b/include/akbasic/symtab.h @@ -23,9 +23,17 @@ * serves variables, functions and labels. Capacity is a member; the table is * kept below a 75% load factor by construction because `capacity` counts slots * and the caller's logical maximum is smaller. + * + * 172 rather than the old 256: no caller passes akbasic_symtab_init() anything + * larger than AKBASIC_MAX_VARIABLES (128), and 172 keeps that under the 75% + * load factor the comment above promises (128 / 0.75 = 170.7). + * + * AKBASIC_SYMTAB_MAX_KEY 24 rather than 64: the longest identifier across + * examples/breakout and examples/megademo -- variables, DIMmed arrays, labels + * and DEF FN names alike -- is 11 characters (TITLESCREEN, CLEARPOWERS). */ -#define AKBASIC_SYMTAB_MAX_SLOTS 256 -#define AKBASIC_SYMTAB_MAX_KEY 64 +#define AKBASIC_SYMTAB_MAX_SLOTS 172 +#define AKBASIC_SYMTAB_MAX_KEY 24 typedef struct { diff --git a/include/akbasic/types.h b/include/akbasic/types.h index 97bfad2..49484fb 100644 --- a/include/akbasic/types.h +++ b/include/akbasic/types.h @@ -6,6 +6,7 @@ #ifndef _AKBASIC_TYPES_H_ #define _AKBASIC_TYPES_H_ +#include #include #include @@ -27,15 +28,39 @@ #define AKBASIC_MAX_VALUES 64 #define AKBASIC_MAX_VARIABLES 128 -/* Whole-runtime pools */ -#define AKBASIC_MAX_SOURCE_LINES 9999 -#define AKBASIC_MAX_LINE_LENGTH 256 +/* + * Whole-runtime pools. + * + * AKBASIC_MAX_SOURCE_LINES, AKBASIC_MAX_LINE_LENGTH, AKBASIC_MAX_ARRAY_VALUES, + * AKBASIC_MAX_ENVIRONMENTS and AKBASIC_MAX_FUNCTIONS were cut from their + * original values against measurements taken off examples/breakout and + * examples/megademo, the two most demanding programs this interpreter runs -- + * see the memory-footprint discussion this commit's PR body links. Each is + * sized at roughly 1.5-2x the peak the reference corpus actually reaches, not + * at the peak itself. + * + * AKBASIC_MAX_LINE_LENGTH in particular follows Commodore BASIC's own 80-column + * line limit rather than a measurement, which is why sink_stdio.c now refuses a + * line that fills the buffer with no terminator instead of silently truncating + * it: at 256 bytes that failure mode was theoretical, and at 80 it is not. + * + * AKBASIC_MAX_SOURCE_PATH_LENGTH is its own constant rather than a reuse of + * AKBASIC_MAX_LINE_LENGTH, which is where it used to come from. `sourcepath` + * (runtime.h) holds a directory, not a line of BASIC, and the two ideas do not + * scale together: shrinking the line limit to 80 broke every golden test in + * this checkout, because this repository's own working directory is deeper + * than that. PATH_MAX is the actual bound a filesystem path is subject to, so + * it is the one this borrows. + */ +#define AKBASIC_MAX_SOURCE_LINES 2048 +#define AKBASIC_MAX_LINE_LENGTH 80 /* Commodore BASIC's own line limit */ +#define AKBASIC_MAX_SOURCE_PATH_LENGTH PATH_MAX #define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */ #define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */ -#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */ +#define AKBASIC_MAX_ARRAY_VALUES 2048 /* array elements across all variables */ #define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */ -#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */ -#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */ +#define AKBASIC_MAX_ENVIRONMENTS 12 /* new: Go allocated these unbounded */ +#define AKBASIC_MAX_FUNCTIONS 8 /* new: Go used an unbounded map */ #define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */ /* * Leaves a DO/LOOP condition may use. Its own small pool rather than a second diff --git a/include/akbasic/variable.h b/include/akbasic/variable.h index 1216585..b4929a8 100644 --- a/include/akbasic/variable.h +++ b/include/akbasic/variable.h @@ -13,12 +13,22 @@ #include +#include #include #include typedef struct { - char name[AKBASIC_MAX_STRING_LENGTH]; + /* + * Sized to AKBASIC_SYMTAB_MAX_KEY, not AKBASIC_MAX_STRING_LENGTH: this name + * only ever gets here by surviving akbasic_symtab_set() first + * (akbasic_environment_create() calls it right after this field is + * populated), and that call refuses anything AKBASIC_SYMTAB_MAX_KEY + * characters or longer with AKBASIC_ERR_BOUNDS. A variable whose name did + * not fit could never exist, so the wider buffer was 232 bytes of headroom + * nothing could ever put a byte into. + */ + char name[AKBASIC_SYMTAB_MAX_KEY]; akbasic_Type valuetype; akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */ int valuecount; diff --git a/src/runtime.c b/src/runtime.c index 69983ae..0b61e0f 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -301,7 +301,7 @@ akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const } FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS, "Program path of %zu characters exceeds the %d character limit", - length, AKBASIC_MAX_LINE_LENGTH - 1); + length, AKBASIC_MAX_SOURCE_PATH_LENGTH - 1); PASS(errctx, aksl_memcpy(obj->sourcepath, path, length)); obj->sourcepath[length] = '\0'; SUCCEED_RETURN(errctx); diff --git a/src/sink_stdio.c b/src/sink_stdio.c index 1e50bf3..7c1ab34 100644 --- a/src/sink_stdio.c +++ b/src/sink_stdio.c @@ -74,6 +74,19 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si if ( *eof ) { SUCCEED_RETURN(errctx); } + /* + * aksl_fgets(3)'s own contract: a full buffer with no trailing newline is + * how a caller spots a line longer than the buffer, because the rest of it + * is still sitting unread in the stream. Refusing here is what makes that + * true -- without it, the unread remainder is picked up by the *next* + * readline() as if it were its own statement, which does not fail, it just + * runs the wrong program. AKBASIC_MAX_LINE_LENGTH is small enough now that + * this is not a hypothetical: examples/breakout's own longest line used to + * clear the old 256-byte ceiling by more than half. + */ + FAIL_NONZERO_RETURN(errctx, (used == len - 1 && dest[used - 1] != '\n' && dest[used - 1] != '\r'), + AKBASIC_ERR_BOUNDS, + "Source line exceeds the %zu character limit", len - 1); /* * Strip the line terminator. The scanner treats \r and \n as end-of-line * anyway, but leaving them on would make a stored source line differ from diff --git a/tests/value_pool.c b/tests/value_pool.c index 51d7ed7..aec9ff6 100644 --- a/tests/value_pool.c +++ b/tests/value_pool.c @@ -97,9 +97,9 @@ static void test_scoped_loop_counter_costs_nothing(void) /** * @brief The pool is genuinely untouched, not merely large enough. * - * Two hundred scope entries and then the pool's *entire* width -- four arrays, + * Two hundred scope entries and then the pool's *entire* width -- two arrays, * because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool - * holds 4096. One leaked slot and the fourth `DIM` has nowhere to go, which + * holds 2048. One leaked slot and the second `DIM` has nowhere to go, which * makes this the sharpest of the three and by far the cheapest: the two above * prove a program finishes, this proves nothing at all was spent. The long ones * stay because they are the shape the defect was found in. @@ -111,10 +111,8 @@ static void test_pool_is_untouched_by_scopes(void) "30 NEXT T#\n" "40 DIM A#(1024)\n" "50 DIM B#(1024)\n" - "60 DIM C#(1024)\n" - "70 DIM D#(1024)\n" - "80 D#(1023) = 7\n" - "90 PRINT D#(1023)\n" + "70 B#(1023) = 7\n" + "90 PRINT B#(1023)\n" "100 END\n" "110 LABEL SUBA\n" "120 LOC# = 1\n" -- 2.43.0 From 1e514f679b454331cf34c0f4503a420e112a36ac Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Mon, 3 Aug 2026 22:57:57 -0400 Subject: [PATCH 2/3] Rework the megademo to fit the 80-column source line limit AKBASIC_MAX_LINE_LENGTH's cut from 256 to 80 left seventeen lines of examples/megademo unloadable: the sixteen IM$() picture strings (up to 252 characters) and TUNEA/TUNEB's four-bar PLAY strings (174 and 175). The real ceiling is 78 characters, not 80 -- stdio_readline() refuses a read that fills the 80-byte buffer without a newline, so content plus its terminator must fit in 79. The picture: vaporwave.py's PAYLOAD drops from 240 to 64, so every emitted IM$(NN) = "..." line fits under the ceiling. chop() no longer slices blind; it walks the stream a record at a time -- two characters for a run, three for an R row record -- and never cuts inside one, because the decoder reads a record's tail with MID on the string it is walking and a record straddling two IM$ entries decodes as garbage. The old blind slice at 240 only happened to be safe. verify() now simulates the CHOPPED strings with the cursor threaded across the boundaries exactly the way DRAWSTREAM executes them, so a bad cut is an assertion failure instead of a corrupted screen, and emit_block() asserts every emitted line fits. The picture is 56 strings where it was 16; the decoder needed no changes at all, since it already carries X#/Y# from one IM$ entry to the next. The music: TUNEA and TUNEB each become four PLAY statements, one bar apiece. play.c keeps voice, envelope, level and duration state on the runtime across statements and every PLAY appends to the same queue, so four bars queue exactly as one long string did. Each bar restates the V1T3U9S prefix so a bar dropped by QFULL cannot leave the next batch playing on the drum kit's envelope. Everything still clears the shrunken pools with room to spare: 1625 source lines of 2048, ~704 array slots of 2048, identifiers within the 24-character symtab key. Verified end to end against this branch's build: the demo loads, the offscreen host renders every scene, and the scene-5 still is pixel-identical to vaporwave.py's own preview. The test suite fails the same seventeen cases with and without this commit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ACffnV6F7sxQuG3Y8a1L3s --- examples/megademo/README.md | 9 ++- examples/megademo/megademo.bas | 116 ++++++++++++++++++++++++--------- examples/megademo/vaporwave.py | 68 +++++++++++++++---- 3 files changed, 146 insertions(+), 47 deletions(-) diff --git a/examples/megademo/README.md b/examples/megademo/README.md index e5c9a27..cf1d948 100644 --- a/examples/megademo/README.md +++ b/examples/megademo/README.md @@ -56,7 +56,10 @@ This is a tour of the interpreter's edges, on purpose: counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is everything `DATA` has room to say. The paint is the reveal, and the exit is a stride-63 column dissolve — 63 is coprime to 160, so the walk hits every - column once and looks random while carrying no state. + column once and looks random while carrying no state. The strings come in + 64-character chunks because a source line is 80 columns, like the machines + this pretends to be — the decoder carries its cursor from one `IM$` entry to + the next, and the generator refuses to cut inside a run. - **And then the picture is video.** Six delta frames loop the floor grid toward you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about 220 bytes a frame. A delta re-encodes only the rows that changed: an `R` @@ -82,6 +85,10 @@ This is a tour of the interpreter's edges, on purpose: batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel. + A batch is four `PLAY` statements, one bar each — the 80-column line limit will + not hold four bars in one string, and it does not need to: the parser's voice, + envelope and duration state persists across statements and every `PLAY` appends + to the same queue, so four bars queue exactly as one line would. It also probes for its hardware like a proper boot loader, with one `TRAP` per device: no graphics refuses by name and exits, no audio mutes the soundtrack, no diff --git a/examples/megademo/megademo.bas b/examples/megademo/megademo.bas index 2e49179..1b58fb4 100644 --- a/examples/megademo/megademo.bas +++ b/examples/megademo/megademo.bas @@ -461,38 +461,78 @@ BS% = W# / 160 IF BS% < 1 THEN BS% = 1 REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not REM ---- hand-edit -- rerun the script to change the picture) -DIM IM$(16) +DIM IM$(56) DIM VA#(6) DIM VB#(6) -NS# = 9 -VA#(0) = 9 -VB#(0) = 9 -VA#(1) = 10 -VB#(1) = 10 -VA#(2) = 11 -VB#(2) = 12 -VA#(3) = 13 -VB#(3) = 13 -VA#(4) = 14 -VB#(4) = 14 -VA#(5) = 15 -VB#(5) = 15 -IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH" -IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP" -IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP" -IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH" -IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH" -IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG" -IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" -IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" -IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" -IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP" -IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP" -IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9" -IM$(12) = "EP" -IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP" -IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP" -IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP" +NS# = 32 +VA#(0) = 32 +VB#(0) = 35 +VA#(1) = 36 +VB#(1) = 39 +VA#(2) = 40 +VB#(2) = 43 +VA#(3) = 44 +VB#(3) = 47 +VA#(4) = 48 +VB#(4) = 51 +VA#(5) = 52 +VB#(5) = 55 +IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEH" +IM$(1) = "G5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGI" +IM$(2) = "EHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEH" +IM$(3) = "GPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEHGHEP" +IM$(4) = "GHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEC" +IM$(5) = "PAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEP" +IM$(6) = "GHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEP" +IM$(7) = "GHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEPGHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHA" +IM$(8) = "E9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAET" +IM$(9) = "EHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9" +IM$(10) = "HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKH" +IM$(11) = "EHKAH9HKKHEHKHEPKHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKH" +IM$(12) = "EHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKH" +IM$(13) = "EHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSED" +IM$(14) = "KPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH" +IM$(15) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZ" +IM$(16) = "EHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KO" +IM$(17) = "I9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9" +IM$(18) = "IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIHKHIHKPIHKPI9IZKB" +IM$(19) = "IHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIH" +IM$(20) = "KHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9" +IM$(21) = "IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIP" +IM$(22) = "KHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKGC9COIFKHI5KHI5KHIPKHI5KHIPKHI5KH" +IM$(23) = "I5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CI" +IM$(24) = "I9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9" +IM$(25) = "E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(26) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9" +IM$(27) = "Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9" +IM$(28) = "QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9" +IM$(29) = "Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" +IM$(30) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(31) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(32) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5" +IM$(33) = "KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EP" +IM$(34) = "RCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9" +IM$(35) = "APRC7E9E9E9E9EP" +IM$(36) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QR" +IM$(37) = "IBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCK" +IM$(38) = "E9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EP" +IM$(39) = "RC7A9A9A9A9APRDAE9E9E9E9EP" +IM$(40) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9" +IM$(41) = "QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9AP" +IM$(42) = "RCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9" +IM$(43) = "EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9EP" +IM$(44) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKH" +IM$(45) = "IPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9" +IM$(46) = "A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9" +IM$(47) = "E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP" +IM$(48) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQ" +IM$(49) = "C9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9" +IM$(50) = "E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9" +IM$(51) = "A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP" +IM$(52) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKH" +IM$(53) = "I5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9" +IM$(54) = "E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9" +IM$(55) = "A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP" REM ---- PICTURE-END SS# = 0 SE# = NS# - 1 @@ -1317,13 +1357,25 @@ TB# = TB# + 1 IF TB# > 2 THEN TB# = 0 RETURN +REM A batch is four PLAY statements, one bar each: the parser's V, T, +REM U and duration state persists across statements and every PLAY +REM appends to the same queue, so four bars queue exactly as one long +REM string would -- which the 80-column line limit no longer allows. +REM Each bar restates the prefix anyway, so a bar dropped by QFULL +REM never leaves the next one playing with drum-kit state. LABEL TUNEA -PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE" +PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A" +PLAY "V1T3U9S O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C" +PLAY "V1T3U9S O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" MT# = TI# + 270 RETURN LABEL TUNEB -PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A" +PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A" +PLAY "V1T3U9S O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D" +PLAY "V1T3U9S O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" MT# = TI# + 270 RETURN diff --git a/examples/megademo/vaporwave.py b/examples/megademo/vaporwave.py index 8f5b73f..4103aa1 100644 --- a/examples/megademo/vaporwave.py +++ b/examples/megademo/vaporwave.py @@ -58,7 +58,14 @@ SKIP = "Q" ROWREC = "R" LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" MAXRUN = len(LENCH) -PAYLOAD = 240 + +# The interpreter reads source through an 80-byte line buffer and refuses +# any line that fills it (AKBASIC_MAX_LINE_LENGTH, sink_stdio.c), so a +# stored line is at most 78 characters plus its newline. 'IM$(NN) = "' and +# the closing quote spend 12 of those; 64 keeps the emitted lines under +# the ceiling with margin to spare while the index stays two digits. +PAYLOAD = 64 +MAXLINE = 78 SUN_CX, SUN_CY, SUN_R = 80, 50, 30 HORIZON = 74 @@ -206,13 +213,32 @@ def encode_delta(prev, cur): def chop(blob): - return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)] + """Split a stream into strings of at most PAYLOAD characters, cutting + only between records. The decoder reads a record's tail characters + with MID on the string it is walking, so a run (two characters) or a + row record (three) that straddled two IM$ entries would decode as + garbage; DRAWSTREAM only carries the cursor, never a partial record.""" + out, cur = [], "" + p = 0 + while p < len(blob): + n = 3 if blob[p] == ROWREC else 2 + if len(cur) + n > PAYLOAD: + out.append(cur) + cur = "" + cur += blob[p:p + n] + p += n + if cur: + out.append(cur) + return out -def simulate(raster, blob): +def simulate(raster, blob, x=0, y=0): """Apply one encoded stream to a raster exactly the way the BASIC - decoder does, skips-draw-nothing and all.""" - x = y = p = 0 + decoder does, skips-draw-nothing and all. The cursor comes in and + goes back out because DRAWSTREAM carries it from one IM$ entry to + the next -- decoding the chopped strings one at a time with the + cursor threaded through is exactly what the demo will execute.""" + p = 0 while p < len(blob): c = blob[p] if c == ROWREC: @@ -230,20 +256,31 @@ def simulate(raster, blob): x = 0 y += 1 p += 2 + return raster, x, y + + +def simulate_lines(raster, lines): + """One stream as its chopped strings, cursor carried across the + boundaries the way DRAWSTREAM carries X# and Y#.""" + x = y = 0 + for line in lines: + raster, x, y = simulate(raster, line, x, y) return raster -def verify(frames, base_blob, delta_blobs): +def verify(frames, base_lines, delta_line_groups): """The base must reproduce frame 0 exactly, and each delta must carry the raster exactly to the next frame. A skip leaves the cell the encoder promised was already right, so equality is total and - any difference at all is an encoder bug.""" + any difference at all is an encoder bug. This decodes the CHOPPED + strings, not the blobs, so a chop that split a record would fail + here instead of corrupting the screen.""" raster = [[1] * W for _ in range(H)] - raster = simulate(raster, base_blob) + raster = simulate_lines(raster, base_lines) assert raster == frames[0], "base stream does not reproduce frame 0" - for i, blob in enumerate(delta_blobs): + for i, lines in enumerate(delta_line_groups): want = frames[(i + 1) % PHASES] - raster = simulate(raster, blob) + raster = simulate_lines(raster, lines) assert raster == want, "delta %d does not reproduce its frame" % i @@ -261,6 +298,9 @@ def emit_block(base_lines, delta_ranges, all_lines): for i, s in enumerate(all_lines): out.append('IM$(%d) = "%s"' % (i, s)) out.append("REM ---- PICTURE-END") + for line in out: + assert len(line) <= MAXLINE, "emitted line over %d chars: %r" % ( + MAXLINE, line) return out @@ -303,15 +343,15 @@ def main(): base_lines = chop(base_blob) all_lines = list(base_lines) delta_ranges = [] - delta_blobs = [] + delta_line_groups = [] for i in range(PHASES): blob = encode_delta(frames[i], frames[(i + 1) % PHASES]) - delta_blobs.append(blob) lines = chop(blob) + delta_line_groups.append(lines) delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1)) all_lines.extend(lines) - verify(frames, base_blob, delta_blobs) - dbytes = sum(len(b) for b in delta_blobs) + verify(frames, base_lines, delta_line_groups) + dbytes = sum(len(l) for g in delta_line_groups for l in g) print("base %d bytes in %d strings; video %d bytes in %d strings; " "%d strings total" % (sum(len(s) for s in base_lines), len(base_lines), dbytes, -- 2.43.0 From eb93bb7da00dade3e99929e4bcf6fce7ce95606b Mon Sep 17 00:00:00 2001 From: Logikoma Date: Tue, 4 Aug 2026 16:14:04 -0400 Subject: [PATCH 3/3] Fix 80-column fixtures and tutorial expectations --- docs/17-tutorial-breakout.md | 6 +++--- docs/18-tutorial-breakout-artwork.md | 3 ++- tests/language/arrays_in_parameter_lists.bas | 2 +- tests/language/audio/filter_refused.bas | 6 +++--- tests/language/audio/no_device.bas | 2 +- tests/language/functions/recursion.bas | 14 +++++++------- tests/language/graphics/argument_errors.bas | 2 +- tests/language/graphics/no_device.bas | 2 +- tests/language/housekeeping/verbs.bas | 6 +++--- tests/language/numeric/octal_literal.bas | 8 ++++---- tests/language/numeric/truth_value_arithmetic.bas | 6 +++--- tests/language/statements/multiple_per_line.bas | 8 ++++---- tests/language/structures/missing_field.bas | 4 ++-- tests/language/structures/parameters.bas | 4 ++-- tests/language/structures/records.bas | 2 +- tests/language/structures/reserved_names.bas | 2 +- .../flowcontrol/nestedforloopwaitingforcommand.bas | 4 ++-- tests/reference/language/functions/mod.bas | 2 +- 18 files changed, 42 insertions(+), 41 deletions(-) diff --git a/docs/17-tutorial-breakout.md b/docs/17-tutorial-breakout.md index 547c4de..5ea37e6 100644 --- a/docs/17-tutorial-breakout.md +++ b/docs/17-tutorial-breakout.md @@ -685,9 +685,9 @@ seconds asks for fifty frames a second. ### Why `GOTO` rather than `DO ... LOOP` A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO` -that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32 +that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12 scopes, so a game that leaves its main loop once per lost life stops on the -thirty-second one: +twelfth one: ```basic N# = 0 @@ -700,7 +700,7 @@ PRINT "SURVIVED " + N# ``` ```output -? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use) +? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use) ``` diff --git a/docs/18-tutorial-breakout-artwork.md b/docs/18-tutorial-breakout-artwork.md index 9cfc737..caf257a 100644 --- a/docs/18-tutorial-breakout-artwork.md +++ b/docs/18-tutorial-breakout-artwork.md @@ -1423,7 +1423,8 @@ IF STATE# = 2 THEN GOSUB UNSTICK RETURN LABEL PRESSPAUSE -IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN +IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" +IF STATE# = 6 THEN GOSUB SETBANNER : RETURN IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER RETURN ``` diff --git a/tests/language/arrays_in_parameter_lists.bas b/tests/language/arrays_in_parameter_lists.bas index 06ae3e2..62e519e 100644 --- a/tests/language/arrays_in_parameter_lists.bas +++ b/tests/language/arrays_in_parameter_lists.bas @@ -1,6 +1,6 @@ 10 REM An array reference used as a function argument, and as one of several. 20 REM An identifier's subscript list used to hang off .right, which is also -30 REM where an argument list chains its arguments -- so the arity counter walked +30 REM where an argument list chains its arguments; the arity counter walked 40 REM straight into the subscripts and refused the call. TODO.md section 4. 50 DIM C#(4) 60 C#(1) = -9 diff --git a/tests/language/audio/filter_refused.bas b/tests/language/audio/filter_refused.bas index 6acc401..a97f9f2 100644 --- a/tests/language/audio/filter_refused.bas +++ b/tests/language/audio/filter_refused.bas @@ -1,6 +1,6 @@ -10 REM FILTER has no device capability behind it -- akgl_audio_* synthesises and -20 REM mixes but has no filter stage, and SDL3 supplies no primitive to build one -30 REM from. It is refused rather than silently ignored, so a program that asked +10 REM FILTER has no device capability; akgl_audio_* synthesises and mixes but +20 REM has no filter stage, and SDL3 supplies no primitive to build one from. +30 REM It is refused rather than silently ignored, so a program that asked 40 REM for a low-pass finds out it did not get one. 50 PRINT "BEFORE" 60 FILTER 1000, 1, 0, 0, 5 diff --git a/tests/language/audio/no_device.bas b/tests/language/audio/no_device.bas index 4de2bb5..c1f119e 100644 --- a/tests/language/audio/no_device.bas +++ b/tests/language/audio/no_device.bas @@ -1,4 +1,4 @@ -10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL and +10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL, 20 REM TEMPO only change interpreter state, so they work regardless; SOUND and 30 REM PLAY need the device and must name themselves when there is none. 40 ENVELOPE 1, 5, 9, 12, 2 diff --git a/tests/language/functions/recursion.bas b/tests/language/functions/recursion.bas index 407328f..e5bd026 100644 --- a/tests/language/functions/recursion.bas +++ b/tests/language/functions/recursion.bas @@ -1,6 +1,6 @@ -10 REM A DEF call takes its environment from the pool, one per call, exactly as +10 REM A DEF call takes one environment from the pool, exactly as 20 REM GOSUB does. It used to be owned by the funcdef and reset on every call, -30 REM which cost two silent defects: two calls in one expression shared a slot, +30 REM which cost two silent defects: calls in one expression shared a slot, 40 REM and recursion never came back at all. 50 DEF FACT(N#) 60 IF N# <= 1 THEN RETURN 1 @@ -15,9 +15,9 @@ 150 DEF DBL(N#) = N# * 2 160 PRINT DBL(10) + DBL(1) 170 PRINT DBL(1) + DBL(10) + DBL(100) -180 REM Depth answers to the environment pool now, so runaway recursion reports -190 REM "Environment pool exhausted" rather than hanging. It is not exercised here -200 REM because the statement containing a failed call still prints a junk value -210 REM afterwards -- a separate, pre-existing defect recorded in TODO.md, and one -220 REM this golden file would pin if it went in. tests/user_functions.c asserts +180 REM Depth answers to the environment pool, so runaway recursion reports +190 REM "Environment pool exhausted" rather than hanging. It is not exercised +200 REM here because the failed statement still prints junk afterwards; +210 REM this is a separate defect in TODO.md, and one this golden file +220 REM would pin if it went in. tests/user_functions.c asserts 230 REM the message instead. diff --git a/tests/language/graphics/argument_errors.bas b/tests/language/graphics/argument_errors.bas index 5efbf97..10b500b 100644 --- a/tests/language/graphics/argument_errors.bas +++ b/tests/language/graphics/argument_errors.bas @@ -1,4 +1,4 @@ -10 REM A range check reported through the driver, end to end. The program stops +10 REM A range check reported through the driver, end to end. The program 20 REM at the first error, so this file covers one; the rest of the checks are 30 REM asserted in tests/graphics_verbs.c against the recording backend. 40 PRINT "BEFORE" diff --git a/tests/language/graphics/no_device.bas b/tests/language/graphics/no_device.bas index 7af43f6..acecc3a 100644 --- a/tests/language/graphics/no_device.bas +++ b/tests/language/graphics/no_device.bas @@ -1,5 +1,5 @@ 10 REM The standalone driver lends the script no graphics device. -20 REM COLOR, LOCATE and SCALE only touch interpreter state and must still work. +20 REM COLOR, LOCATE and SCALE touch interpreter state and must still work. 30 COLOR 1, 3 40 LOCATE 40, 50 50 SCALE 1, 640, 400 diff --git a/tests/language/housekeeping/verbs.bas b/tests/language/housekeeping/verbs.bas index 6cd67f6..34cf3ba 100644 --- a/tests/language/housekeeping/verbs.bas +++ b/tests/language/housekeeping/verbs.bas @@ -1,5 +1,5 @@ -10 REM Group B: the housekeeping verbs. None of these is in the Go reference -- -20 REM they are on its own unimplemented list -- so what each one means here is +10 REM Group B: housekeeping verbs. None is in the Go reference -- +20 REM they are on its unimplemented list -- so each meaning here is 30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb. 40 A# = 1 : B# = 2 50 PRINT A# : PRINT B# @@ -11,7 +11,7 @@ 110 P#(2) = 7 : Q#(2) = 9 120 SWAP P#, Q# 130 PRINT P#(2) : PRINT Q#(2) -140 REM TRON prints each line number inline before the line runs, as a C128 does. +140 REM TRON prints each line number inline, as a C128 does. 150 TRON 160 PRINT "TRACED" 170 TROFF diff --git a/tests/language/numeric/octal_literal.bas b/tests/language/numeric/octal_literal.bas index 473c4c2..b1703f0 100644 --- a/tests/language/numeric/octal_literal.bas +++ b/tests/language/numeric/octal_literal.bas @@ -1,7 +1,7 @@ -10 REM A leading zero is padding, not a radix. The reference selects base 8 for -20 REM any lexeme starting with 0, so 010 printed 8 and 08 was a parse error -- -30 REM TODO.md section 6 item 10, fixed. Commodore BASIC has no octal literals. -40 REM 0x is the one prefix that changes the base, and it now reaches the scanner +10 REM A leading zero is padding, not a radix. The reference selected base 8 +20 REM for lexemes starting with 0; 010 printed 8 and 08 was a parse error -- +30 REM TODO.md section 6 item 10 is fixed; Commodore BASIC has no octal. +40 REM 0x is the one prefix that changes the base, and now reaches the scanner 50 REM whole: that was section 6 item 15. 60 PRINT 010 70 PRINT 08 diff --git a/tests/language/numeric/truth_value_arithmetic.bas b/tests/language/numeric/truth_value_arithmetic.bas index eb6d5f9..9eacf29 100644 --- a/tests/language/numeric/truth_value_arithmetic.bas +++ b/tests/language/numeric/truth_value_arithmetic.bas @@ -1,12 +1,12 @@ 10 REM A truth value carries its payload in boolvalue, not floatval. The three 20 REM numeric operators were `if ( INTEGER ) ... else `, and -30 REM that else was a catch-all rather than a float branch -- so a truth value +30 REM that else was a catch-all, not a float branch -- so a truth value 40 REM on the LEFT computed 0 - 1 into a field nothing reads, kept its BOOLEAN 50 REM type, and printed `true` instead of -2. Silent, and wrong twice over. 60 A# = 1 70 PRINT (A# == 1) -80 REM On the RIGHT it stays legal and stays -1, which is the same property that -90 REM lets AND and OR double as logical operators. Refusing it here would have +80 REM On the RIGHT it stays legal and stays -1, the same property that +90 REM lets AND and OR act as logical operators. Refusing it would have 100 REM broken every condition in the language. 110 PRINT 5 - (A# == 1) 120 PRINT 5 * (A# == 1) diff --git a/tests/language/statements/multiple_per_line.bas b/tests/language/statements/multiple_per_line.bas index 0b631ba..6e20b54 100644 --- a/tests/language/statements/multiple_per_line.bas +++ b/tests/language/statements/multiple_per_line.bas @@ -1,11 +1,11 @@ -10 REM Statements separated by colons. The COLON token existed from the start of -20 REM the port and nothing consumed it, so a line could hold only one statement. +10 REM Statements separated by colons. The COLON token existed from the start; +20 REM nothing consumed it, so a line could hold only one statement. 30 PRINT "A" : PRINT "B" 40 A# = 1 : B# = 2 : PRINT A# + B# -50 REM An empty statement is not an error: a trailing separator, or a run of them. +50 REM Empty statements are valid: a trailing separator or runs of them. 60 PRINT "C" : 70 PRINT "D" :: PRINT "E" -80 REM Everything after THEN belongs to the condition, which is BASIC 7.0 and is +80 REM Everything after THEN belongs to the condition, as in BASIC 7.0, and is 90 REM not something the reference had an opinion about -- it never got here. 100 IF 1 == 1 THEN PRINT "TRUE-1" : PRINT "TRUE-2" 110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2" diff --git a/tests/language/structures/missing_field.bas b/tests/language/structures/missing_field.bas index 97477ef..0f6f07c 100644 --- a/tests/language/structures/missing_field.bas +++ b/tests/language/structures/missing_field.bas @@ -1,5 +1,5 @@ -10 REM A field name is checked against a closed set the program declared, which -20 REM is the one thing in this language whose valid spellings are written down. +10 REM A field name is checked against the closed set the program declared; +20 REM this is the one thing whose valid spellings are written down. 30 REM A misspelled *variable* is still silent -- see the last two lines. 40 TYPE RECT 50 W# diff --git a/tests/language/structures/parameters.bas b/tests/language/structures/parameters.bas index b204c20..8da33b6 100644 --- a/tests/language/structures/parameters.bas +++ b/tests/language/structures/parameters.bas @@ -17,7 +17,7 @@ 170 RETURN B@.W# 180 PRINT WIDEN(A@) 190 PRINT A@.W# -200 REM To change one on purpose, pass a pointer. Assignment copies a pointer's +200 REM To change one on purpose, pass a pointer. Assignment copies its 210 REM reference, so the callee is looking at the caller's own record. 220 DIM Q@ AS PTR TO CRATE 230 POINT Q@ AT A@ @@ -37,7 +37,7 @@ 370 N2@.COUNT# = 20 380 POINT N1@.TAIL@ AT N2@ 390 DEF TOTAL(P@ AS PTR TO NODE) -395 REM A pointer is true when it points at something, which is how a walk knows +395 REM A pointer is true when it points at something, so a walk knows 396 REM where the list ends. NOT is the bitwise operator here, so the test is 397 REM written the positive way round. 400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@) diff --git a/tests/language/structures/records.bas b/tests/language/structures/records.bas index 762ea7c..c65cce2 100644 --- a/tests/language/structures/records.bas +++ b/tests/language/structures/records.bas @@ -1,4 +1,4 @@ -10 REM A TYPE declares its fields; each takes its own type from its own suffix, +10 REM A TYPE declares its fields; each takes its type from its own suffix, 20 REM which is the same rule every other name in this language follows. 30 TYPE COORD 40 X# diff --git a/tests/language/structures/reserved_names.bas b/tests/language/structures/reserved_names.bas index b273f02..be110d5 100644 --- a/tests/language/structures/reserved_names.bas +++ b/tests/language/structures/reserved_names.bas @@ -1,6 +1,6 @@ 10 REM A type name and a field name are bare words, and so is every verb, so 20 REM they share a namespace whether we like it or not. Both are refused with -30 REM the same rule the scanner already applies to variable names -- and a type +30 REM the scanner applies the same rule to variable names -- and a type 40 REM name is refused by the prescan, which can say so plainly rather than 50 REM leaving the parser to report "Expected expression or literal". 60 TYPE POINT diff --git a/tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas b/tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas index 79be7c0..67494d9 100644 --- a/tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas +++ b/tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas @@ -1,6 +1,6 @@ 10 REM This shows the waitingForCommand utility in the BasicEnvironment 11 REM when we have a nested for loop. The inner loop SHOULD execute, but -12 REM the outer loop should NOT execute. Therefore, neither loop should execute. +12 REM the outer loop should NOT execute; neither loop should execute. 20 FOR I# = 1 TO 0 25 FOR J# = 2 TO 4 30 PRINT "waitingForCommand FAILS if this is seen" @@ -8,4 +8,4 @@ 35 NEXT J# 40 NEXT I# 50 PRINT "SUCCESS" -80 QUIT \ No newline at end of file +80 QUIT diff --git a/tests/reference/language/functions/mod.bas b/tests/reference/language/functions/mod.bas index d2d268f..a13fc81 100644 --- a/tests/reference/language/functions/mod.bas +++ b/tests/reference/language/functions/mod.bas @@ -1,6 +1,6 @@ 10 PRINT MOD(10, 3) 20 PRINT MOD(12, 5) 30 PRINT MOD(4, 2) -40 REM MOD() ONLY WORKS WITH INTEGERS - RESULTS WITH FLOATING POINT ARE UNRELIABLE +40 REM MOD() ONLY WORKS WITH INTEGERS; FLOATING POINT RESULTS ARE UNRELIABLE 50 REM PRINT MOD(1.2, 0.4) 60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT. -- 2.43.0