galaga-tutorial
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 49d4359343 |
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 <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> |
|||
|
b434be1901
|
Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
3342f2b569
|
Make GRAPHIC select the text plane
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net> |
|||
|
f626d7985f
|
Let a program ask how big the text grid is
`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> |
|||
|
771485e003
|
Forward WINDOW through the tee sink
`akbasic_sink_init_tee()` wired write, writeln, readline, clear and -- conditionally -- moveto, and never `window`. The standalone AKGL frontend runs the interpreter against a tee, so the fully implemented `sink_window()` underneath it was unreachable: `WINDOW 0, 0, 20, 4` answered "WINDOW needs a text device with a character grid, and this one has none" on the one build that has a character grid. It reads as an oversight rather than a decision, because `moveto` directly above it is forwarded with reasoning that applies unchanged. `tee_window()` is that function, modelled on `tee_moveto()`, offered only when a half can take it so the refusal still reads correctly through a stdio-only pair. **Writing the test found a second one.** Both optional entry points are assigned conditionally, and neither initializer cleared them first -- so a caller with a sink on the stack got whatever was in that memory, and `CHAR` and `WINDOW` decide whether they can act by testing those pointers for NULL. Both `akbasic_sink_init_tee()` and `akbasic_sink_init_stdio()` now clear them. An initializer that leaves a field alone is not an initializer. tests/sink_tee.c gains a stand-in sink with a grid -- the two stdio halves have neither entry point, which is exactly why they could not show that either is forwarded -- and asserts both directions, the arguments arriving intact, and that a grid-less pair still offers neither. Verified end to end: `WINDOW 0, 0, 20, 4` then `PRINT` succeeds under build-akgl/basic and still refuses by name under build/basic. What this does *not* fix is that a drawing still has to be re-issued every frame: the frontend never clears and SDL is double-buffered, so a drawing issued once appears in one buffer only. Shrinking the text area makes the rest of the window the program's; keeping something there is still the program's job. That half is documented rather than fixed, and is filed as TODO.md section 9 item 5. TODO.md section 6 item 31, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> |
|||
|
4e188b2794
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
|