Add two Breakout examples and the tutorials that build them
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Two complete games in `examples/breakout/`, both 100% BASIC: `characters/` draws its wall in the text grid with two `DATA` sprites for the ball and paddle, and `sprites/` loads CC0 artwork and captures its whole screen with `SSHAPE`/`SPRSAV`. They take opposite shapes for reasons that are entirely this interpreter's, which is what the chapters are for. `docs/17-tutorial-breakout.md` and `docs/18-tutorial-breakout-artwork.md` build each one a step at a time, and end in a checklist of the rules a real program runs into: create every name before the loop starts, write a text row whole, loop with `GOTO` rather than `DO`, put the float on the left. Every trap is a runnable block with its own output rather than a claim -- the value pool dying at four thousand names, the skipped `BEGIN` block that breaks its caller's `RETURN`, `SSHAPE` ignoring a subscript, `READ`'s single cursor. Five figures, generated from the listings beside them by `docs_screenshots`, and a `breakout_art` setup so the ones that load artwork load the example's own. The character game's wall cannot be photographed -- the screenshot host omits the text layer on purpose -- so it is shown as compared output instead. `docs/07-sound.md` never said `SOUND`'s frequency is a SID register value rather than hertz, which both games depend on. It says so now, with the conversion from `src/audio_tables.c:84`. `TODO.md` gains the thirteen defects the two games turned up -- §6 items 30 to 33 and all of §9 -- each with a reduction that fits on a screen, the file and line of the cause, and what a fix would touch. Verified: `docs_examples` passes in both build configurations, `docs_screenshots --check` re-renders all thirteen figures and byte-compares them, the full 109-test suite passes in both builds, every quoted fragment was checked to appear verbatim in the listing it came from, and every relative link and anchor resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
424
TODO.md
424
TODO.md
@@ -1858,6 +1858,147 @@ the reference's semantics reproduced faithfully; the third is the port's own.
|
||||
reentrant" and "do not grow the runtime by a third for a scratch buffer" are both right
|
||||
and picking between them is a decision.
|
||||
|
||||
### Found while writing a game against the interpreter
|
||||
|
||||
A complete Breakout, sprites and text grid and keyboard, running for minutes at a time. Nothing
|
||||
in either corpus runs for minutes, which is why the first of these had never been seen.
|
||||
|
||||
30. **A variable created inside a scope costs pool slots that never come back, so a long-running
|
||||
program has a hard budget of 4096 name creations.** `akbasic_ValuePool` is a bump allocator
|
||||
on purpose (`include/akbasic/value.h:30`), and the reason given is that "nothing in BASIC
|
||||
destroys a variable". **Scope exit destroys variables.**
|
||||
`akbasic_runtime_prev_environment()` (`src/runtime.c:121`) marks a popped environment's
|
||||
variables `used = false`; `akbasic_runtime_new_variable()` (`src/runtime.c:31`) `memset`s
|
||||
the slot it hands back, clearing `values` and `valuecount`; and `akbasic_variable_init()`
|
||||
(`src/variable.c:98`) therefore takes *fresh* slots for a variable whose old ones are still
|
||||
counted against `obj->next`. Nothing decrements it, so every `GOSUB` that creates a local
|
||||
leaks the local's slots for the life of the run.
|
||||
|
||||
Reduced, and it fails on both builds:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
`? 110 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots`,
|
||||
at `LOC# = 1`, on the 4091st call -- 4096 less the handful of names the program itself
|
||||
declared. Both builds, same line, same count. Declare `LOC#` alongside `X#` and the same
|
||||
6000 calls cost nothing at all. A `FOR` counter behaves the same way: name one that does
|
||||
not already exist and every entry to the loop spends a slot.
|
||||
|
||||
**What it costs a program:** a game loop that creates one name per tick is dead in half a
|
||||
minute, and the error names the line that happened to be unlucky rather than the line that
|
||||
caused it. Breakout ran twenty-five seconds before this was understood. The workaround is
|
||||
real and not onerous -- declare every name, loop counters included, at the top -- but it
|
||||
has to be *known*, and right now the only way to learn it is to hit it.
|
||||
|
||||
Three ways out, in increasing order of work. Reclaim on scope exit: give the pool a mark
|
||||
and release, take the mark when an environment is pushed and rewind to it when it pops,
|
||||
which is exactly the discipline the environments themselves already have and is why they
|
||||
are a pool in the first place. Or keep the slice on the recycled variable: stop the
|
||||
`memset` in `new_variable()` from clearing `values`/`valuecount` and let
|
||||
`variable_init()`'s existing "reuse the slice when it is already big enough" branch do the
|
||||
work -- much smaller, and it fixes the common case of the same-sized scalar being recreated
|
||||
over and over. Or, at minimum, say so in the manual and in `value.h`'s comment, which
|
||||
currently states the opposite.
|
||||
|
||||
Whichever is chosen, the test is a CTest program that enters and leaves a scope creating a
|
||||
variable tens of thousands of times and returns zero -- and it wants to exist even for the
|
||||
documentation-only outcome, pinned at whatever the real budget turns out to be. Item 33
|
||||
has to be settled with it: while the pool is dry the failure cannot be trapped, so a
|
||||
program cannot even report this one against itself.
|
||||
|
||||
31. **`WINDOW` cannot be reached in the standalone AKGL build.** `akbasic_sink_init_tee()`
|
||||
(`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
|
||||
-- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
|
||||
the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
|
||||
(`src/sink_akgl.c:413`) is unreachable: `WINDOW 0, 0, 10, 10` in the windowed build answers
|
||||
`? RUNTIME ERROR WINDOW needs a text device with a character grid, and this one has none`.
|
||||
The verb is only usable by a host that hands the runtime the akgl sink directly.
|
||||
|
||||
It looks like an oversight rather than a decision, because `moveto` right above it *is*
|
||||
forwarded, and with the same reasoning that would apply here. The fix is a `tee_window()`
|
||||
that forwards to whichever half offers one, offered only when one does, and a case in
|
||||
`tests/sink_tee.c` beside the `moveto` one. §3's "Still missing from the sink" closes by
|
||||
calling `WINDOW` "ordinary work here rather than anything blocked", which is now half true:
|
||||
the work was done and then not plumbed.
|
||||
|
||||
Worth deciding at the same time: **a program still has no way to ask how big the text grid
|
||||
is.** `RGR(1)` and `RGR(2)` give the window in pixels, and nothing gives columns, rows or
|
||||
the cell size, so anything that wants to place a character *and* a sprite at the same spot
|
||||
has to hardcode a cell size measured against the bundled font -- which is what the Breakout
|
||||
in `examples/breakout/characters` does, and it is the one thing in that listing that will break
|
||||
on a different font or window. Two more `RGR()` indices would answer it.
|
||||
|
||||
32. **A character written past a short row's terminator is stored where nothing will render
|
||||
it.** A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
|
||||
writes the character, advances, and terminates -- so `CHAR 1, 40, 1, "#"` on an empty row
|
||||
stores `#` at column 40 with `text[1][0]` still `'\0'`, and `sink_akgl_render()` draws
|
||||
nothing at all. The same call on a row already 50 characters long draws the `#` and erases
|
||||
columns 41 onward, which is the documented truncation and is fine.
|
||||
|
||||
It is the *silent nothing* that is the trap: the write succeeds, the cursor moves, stdout's
|
||||
mirror shows the character, and the window stays blank. Either pad the gap with spaces on
|
||||
a `moveto` past the terminator, so a positioned write always lands, or say plainly beside
|
||||
`CHAR` in `docs/11-verb-reference.md` and deviation 49 that a row is written from column 0
|
||||
or not at all. Padding is the friendlier of the two and costs one loop in `sink_moveto()`;
|
||||
the argument against it is that it makes `moveto` write to the grid, which it currently
|
||||
does not.
|
||||
|
||||
33. **A `TRAP` handler cannot be entered once the value pool is dry, and the error is dropped
|
||||
in silence.** `akbasic_trap_set_error_variables()` (`src/runtime_trap.c:36`) sets `ER#`
|
||||
and `EL#` through `akbasic_runtime_global()` (`:51` and `:53`), which *creates* them if
|
||||
the program never named them. Creating them needs pool slots. So the one error most
|
||||
likely to leave the pool empty -- item 30's -- is the one error whose handler cannot be
|
||||
reached, and because the dispatch fails inside the error path rather than raising, **the
|
||||
program keeps running with the failed statement's effect quietly missing**.
|
||||
|
||||
It is easy to see, and it is much worse than an abort:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
TRAP RANOUT
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
|
||||
LABEL RANOUT
|
||||
PRINT "DIED AT X " + X#
|
||||
QUIT
|
||||
```
|
||||
|
||||
prints `OK 4092`. The handler never ran, nothing was reported, and `X#` is 1908 short of
|
||||
the 6000 the loop counted -- a wrong answer delivered as a right one. Add `ER# = 0` and
|
||||
`EL# = 0` at the top, so the dispatch has nothing left to create, and the same program
|
||||
prints `DIED AT X 4090` from its handler as it should.
|
||||
|
||||
Two things want fixing and they are separable. The dispatch should not allocate: create
|
||||
`ER#` and `EL#` at runtime init, where there is always room, rather than at the moment the
|
||||
program is in trouble -- they are reserved names and §1's other reserved globals are
|
||||
already there. And a failure *inside* the trap dispatch should not be swallowed; if the
|
||||
handler cannot be entered, the original error is the thing to report, and reporting it is
|
||||
what the untrapped path already does correctly.
|
||||
|
||||
`AKBASIC_ERR_BOUNDS` from the pool is documented as "bounded and diagnosable, which is the
|
||||
point" (`include/akbasic/value.h:34`). With a `TRAP` armed it is currently neither.
|
||||
|
||||
## 7. Filing gaps against `libakgl`
|
||||
|
||||
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
|
||||
@@ -2285,3 +2426,286 @@ What remains, in priority order:
|
||||
own body said the fill was filed rather than implemented. Drawing the figure and getting
|
||||
an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented,
|
||||
recorded by the mock, and reached by no verb at all as a result.
|
||||
|
||||
---
|
||||
|
||||
## 9. Defects found by writing a game against it
|
||||
|
||||
`examples/breakout/sprites/breakout.bas` is a complete Breakout — sprite artwork,
|
||||
powerups, a stroke-font HUD — written in this dialect, for this interpreter, by somebody who
|
||||
had not written a line of it before. Eight things came out of that, and they are here for the
|
||||
same reason §8's three are: **none of them was on any list, and all of them were found by
|
||||
using the thing rather than by testing it.** The corpus reaches none of them.
|
||||
|
||||
Ordered by blast radius. Each has a minimal reproduction that fits on a screen; every one was
|
||||
reduced against `build/basic`, the stdio build, unless it says otherwise.
|
||||
|
||||
1. **Every call to a user-defined function leaks value-pool slots, so a program may call one
|
||||
about four thousand times and then die.**
|
||||
|
||||
```basic
|
||||
DEF ADDIT(N#)
|
||||
RETURN N# + 1
|
||||
R# = 0
|
||||
FOR I# = 1 TO 8000
|
||||
R# = ADDIT(I#)
|
||||
NEXT I#
|
||||
```
|
||||
|
||||
```output
|
||||
? 5 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
|
||||
```
|
||||
|
||||
It fails between 4000 and 5000 calls, which is `AKBASIC_MAX_ARRAY_VALUES` (4096,
|
||||
`include/akbasic/types.h:29`) at one slot per call — the call scope's parameter. **Both
|
||||
`DEF` forms leak**; the single-expression `DEF SQR1(X#) = X# * X#` fails at the same point.
|
||||
A `GOSUB` doing the same work runs eight thousand times without complaint, which is the
|
||||
comparison that isolates it.
|
||||
|
||||
`akbasic_valuepool_take()` (`src/value.c:142`) is a bump allocator — `obj->next += count` at
|
||||
`:160` — and **nothing anywhere returns a slot**. The only reset is
|
||||
`akbasic_valuepool_init()`, from `src/runtime.c:209` at startup and
|
||||
`src/runtime_housekeeping.c:53` for `CLR`/`NEW`. Environments *are* released back to their
|
||||
own pool (§1.4, and §6 item 11 says so), but the value slots the variables in them held are
|
||||
not: `src/environment.c:321` takes them through `akbasic_variable_init()` and the release
|
||||
path has no counterpart.
|
||||
|
||||
The consequence is a hard ceiling on a whole language feature. A game calling one function
|
||||
per frame at 30fps gets a little over two minutes. It is why the game in question spells its
|
||||
pseudo-random generator as a `GOSUB` over globals rather than the `DEF` it wants to be.
|
||||
|
||||
Fixing it means giving the pool a free list, or giving each environment a value arena
|
||||
released with it. The second is closer to the house style and to what §1.4 already does for
|
||||
environments; it touches `src/value.c`, `src/variable.c`, `src/environment.c` and
|
||||
`include/akbasic/value.h`. A test belongs in `tests/user_functions.c`: call a `DEF` ten
|
||||
thousand times and then `DIM` an array.
|
||||
|
||||
2. **A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because `FOR`
|
||||
and `DO` create their environment at *parse* time and the skip is decided at *evaluate*
|
||||
time.
|
||||
|
||||
```basic
|
||||
N# = 0
|
||||
LABEL AGAIN
|
||||
N# = N# + 1
|
||||
IF 1 = 0 THEN BEGIN
|
||||
FOR I# = 0 TO 2
|
||||
N# = N# + 1000
|
||||
NEXT I#
|
||||
BEND
|
||||
IF N# < 40 THEN GOTO AGAIN
|
||||
```
|
||||
|
||||
```output
|
||||
? 5 : PARSE ERROR Environment pool exhausted at line 5 (32 in use)
|
||||
```
|
||||
|
||||
Thirty-two skips is `AKBASIC_MAX_ENVIRONMENTS` (`include/akbasic/types.h:31`) at one per
|
||||
skip. Inside a subroutine it announces itself on the very first skip and much more
|
||||
confusingly — the orphaned scope sits between the routine and its caller, so the `RETURN`
|
||||
after the block reports from a routine that plainly *was* entered by a `GOSUB`:
|
||||
|
||||
```basic
|
||||
T# = 0
|
||||
GOSUB DOIT
|
||||
PRINT "came back"
|
||||
END
|
||||
LABEL DOIT
|
||||
IF 1 = 0 THEN BEGIN
|
||||
FOR I# = 0 TO 2
|
||||
T# = T# + 1
|
||||
NEXT I#
|
||||
BEND
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
|
||||
```
|
||||
|
||||
That is the form it was found in, and it costs an evening because the error names the one
|
||||
construct that is not at fault.
|
||||
|
||||
**The cause is an ordering mismatch and it is exact.** `akbasic_parse_do()`
|
||||
(`src/parser_commands.c:266`) and `akbasic_parse_for()` (`:849`) both call
|
||||
`akbasic_runtime_new_environment()` while parsing the line. The block skip is tested in
|
||||
`akbasic_runtime_interpret_line()` (`src/runtime.c:767`), which runs *after* the line has
|
||||
been parsed — so a skipped `FOR` pushes a scope, its execution is skipped, and the `NEXT`
|
||||
that would pop it is skipped too. `BEGIN` itself pushes nothing, which is why a nested
|
||||
`BEGIN`/`BEND` inside a skipped block is harmless, and a `GOSUB` inside one is harmless for
|
||||
the same reason.
|
||||
|
||||
Confirmed for both loop forms: `DO`/`LOOP` in a skipped block fails identically. Confirmed
|
||||
for both kinds of enclosing routine: a multi-line `DEF` reports the same thing.
|
||||
|
||||
A fix has to either push the loop's scope at execution rather than at parse, or have the
|
||||
skip release what parsing pushed. Touches `src/parser_commands.c`, `src/runtime.c` and
|
||||
`tests/structure_verbs.c`, which today only exercises blocks whose bodies are plain
|
||||
statements.
|
||||
|
||||
3. **In the standalone SDL build, nothing the graphics verbs draw can ever be seen.** Not
|
||||
"does not persist" — *never visible at all*, in any frame.
|
||||
|
||||
```basic
|
||||
GRAPHIC 1, 1
|
||||
COLOR 1, 3
|
||||
BOX 1, 100, 300, 700, 500
|
||||
PRINT "THE TEXT SHOWS AND THE BOX DOES NOT"
|
||||
LABEL SPIN
|
||||
GOTO SPIN
|
||||
```
|
||||
|
||||
The text appears; the box never does. Two mechanisms combine, and each alone would be
|
||||
survivable:
|
||||
|
||||
- `akbasic_sink_akgl_render()` fills **every row of the text area, opaque, every frame**
|
||||
(`src/sink_akgl.c:564`), and the area is the whole window — `state->rows = h / cellh` at
|
||||
`:505`. `akbasic_frontend_akgl_pump()` calls it after `akbasic_runtime_run()` and before
|
||||
`SDL_RenderPresent`, so it paints over everything the program drew during those steps.
|
||||
The comment at `:564` explains why it must repaint rather than track dirty rows, and that
|
||||
reasoning is sound; what it does not account for is that the rows it owns are all of them.
|
||||
- `WINDOW` would shrink the text area out of the way, and the akgl sink implements it
|
||||
(`sink_window`, `src/sink_akgl.c:413`) — but **the tee sink in front of it never assigns
|
||||
`obj->window`** (`src/sink_tee.c:143-151` assigns `clear` and conditionally `moveto`, and
|
||||
stops). So `WINDOW` refuses with "needs a text device with a character grid"
|
||||
(`src/runtime_console.c:179`) in the one build that has a character grid.
|
||||
|
||||
The result is that **chapter 6 cannot be run in the interpreter it documents**. Its figures
|
||||
are right because `tools/screenshot.c` deliberately omits the text layer — the comment in
|
||||
that file says so — so the harness that proves the chapter is exactly the harness that hides
|
||||
this. `docs_screenshots` passes and always would.
|
||||
|
||||
Sprites are unaffected: `akbasic_sprite_akgl_render()` runs after the sink, which is why the
|
||||
game draws its entire screen by `SSHAPE`-ing what it drew and installing it with `SPRSAV`.
|
||||
That is a fine trick and it should not be the only way.
|
||||
|
||||
The tee half is three lines and is worth doing whatever is decided about the rest.
|
||||
|
||||
4. **Mixed-type arithmetic is decided by the left operand alone, and nothing says so.**
|
||||
|
||||
```basic
|
||||
A# = 3
|
||||
PRINT A# * 0.45
|
||||
PRINT 0.45 * A#
|
||||
```
|
||||
|
||||
```output
|
||||
0
|
||||
1.350000
|
||||
```
|
||||
|
||||
`akbasic_value_math_plus()` and its siblings branch on `self->valuetype`
|
||||
(`src/value.c:510`, `:512`, `:539`) and convert the right operand to match, so an integer on
|
||||
the left silently truncates a float on the right. **This is inherited, not introduced** —
|
||||
`deps/basicinterpret/basicvalue.go:190-193` has the same shape — and §6 item 5 already fixed
|
||||
the other half of that expression without changing the branch.
|
||||
|
||||
It may well be intended; a dialect is allowed to say the left operand wins, and this one is
|
||||
consistent about it. The defect is that **it is documented nowhere**. Chapter 3's "Numbers"
|
||||
does not mention it, chapter 13 does not list it among the differences, and a C128 promotes
|
||||
to float instead. Nothing fails when you get it wrong — the program computes something else
|
||||
and carries on.
|
||||
|
||||
It cost the game two live bugs before either was noticed. `SPD% = 5.6 + LEVEL# * 0.45`
|
||||
evaluated to a flat 5.6, so no level ever ran faster than level one; and `0 - BLVX%(B#)`,
|
||||
the obvious way to reverse a ball, quantised its velocity to whole pixels on every bounce
|
||||
and bled speed out of it. Both read correctly. Neither produced a diagnostic.
|
||||
|
||||
The cheap fix is documentation: a paragraph in chapter 3 and a row in chapter 13. The other
|
||||
fix is promotion, which would be a real change of semantics and wants its own decision.
|
||||
|
||||
5. **A drawing that spans a 256-line batch boundary is torn, not merely transient.** §5 and
|
||||
chapter 13 record that drawing does not persist across frames. What neither says is that a
|
||||
single uninterrupted sequence of drawing verbs longer than one `akbasic_runtime_run()` budget
|
||||
comes back **half drawn**, because the present in the middle of it discards the buffer — so
|
||||
an `SSHAPE` at the end captures only the statements issued since that present, over whatever
|
||||
the frame before left behind.
|
||||
|
||||
Measured against `AKBASIC_FRONTEND_STEPS_PER_FRAME` of 256, in the SDL build: after
|
||||
synchronising to a jiffy edge, 110 `DRAW` statements in a `FOR` loop (220 executed lines)
|
||||
are captured whole; 125 (250 lines) capture nothing at all; 140 (280 lines) capture the last
|
||||
fifteen. A program cannot see the boundary except by watching `TI#` change, which is what
|
||||
the game's `PACE` routine does and what makes the whole thing workable.
|
||||
|
||||
This is worth a paragraph in chapter 13 beside the existing note, because "redraw it every
|
||||
frame" is the advice there and it is not sufficient advice — the redraw also has to fit.
|
||||
|
||||
6. **`SSHAPE` and `GSHAPE` ignore the subscript on a string array element.** Needs a graphics
|
||||
device, so this one is reduced against `build-akgl/basic`.
|
||||
|
||||
```basic
|
||||
DIM SH$(4)
|
||||
SSHAPE SH$(2), 0, 0, 61, 4
|
||||
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
|
||||
```
|
||||
|
||||
```output
|
||||
[SHAPE:0] []
|
||||
```
|
||||
|
||||
The handle lands in `SH$(0)`; `SH$(2)` stays empty. `GSHAPE SH$(2)` then stamps whatever is
|
||||
in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so a program that keeps
|
||||
several saved shapes in an array gets every one of them resolving to element zero, silently.
|
||||
|
||||
`shape_variable()` (`src/runtime_graphics.c:625`) takes `arg->identifier` and calls
|
||||
`akbasic_environment_get()` — it never evaluates the subscript — and both verbs then use a
|
||||
literal zero: `src/runtime_graphics.c:688` writes with `zerosubscript`, `:724` reads with it.
|
||||
**`SPRSAV` is the counter-example and the model for the fix**: it calls
|
||||
`akbasic_runtime_evaluate()` on its argument (`src/runtime_sprite.c:480`) and handles an
|
||||
array element correctly.
|
||||
|
||||
The game keeps six brick stamps in six separate scalars because of this, and the workaround
|
||||
is not obvious from the failure — every brick simply comes out the colour of the last one
|
||||
captured. `tests/graphics_verbs.c` is where the regression goes.
|
||||
|
||||
7. **`GRAPHIC CLR` is documented and refused.** `docs/06-graphics.md:65` and
|
||||
`docs/11-verb-reference.md:54` both give the form as `GRAPHIC mode | CLR`.
|
||||
|
||||
```basic
|
||||
GRAPHIC CLR
|
||||
```
|
||||
|
||||
```output
|
||||
? 1 : PARSE ERROR 1 at 'CLR', Expected expression or literal
|
||||
```
|
||||
|
||||
`GRAPHIC` parses with `akbasic_parse_arglist` (`src/verbs.c:86`), so `CLR` scans as the `CLR`
|
||||
verb rather than as a keyword argument. `GRAPHIC 5` does the job and is what the game calls.
|
||||
Either the parse handler learns the word or both documents lose it; the second is smaller and
|
||||
the first is what a C128 programmer will type.
|
||||
|
||||
Worth noting because `docs_examples` cannot catch it: every fenced `GRAPHIC CLR` in the
|
||||
documentation is prose, not a tagged runnable block.
|
||||
|
||||
8. **A `DATA` line holding a negative number cannot be executed.** §4 settled that "`DATA` at
|
||||
run time is now a no-op: it is a declaration, and reaching the statement means walking past
|
||||
it." Walking past a negative one raises instead:
|
||||
|
||||
```basic
|
||||
READ A#
|
||||
PRINT "READ GAVE " + A#
|
||||
DATA -5
|
||||
PRINT "REACHED THE LINE AFTER THE DATA"
|
||||
```
|
||||
|
||||
```output
|
||||
READ GAVE -5
|
||||
? 3 : PARSE ERROR Expected literal
|
||||
```
|
||||
|
||||
`READ` handles the value correctly — the negative comes back intact — so this is only the
|
||||
statement's own argument parse, in `akbasic_parse_data()`
|
||||
(`src/parser_commands.c:934`), refusing a leading `-` where the prescan accepted it. The
|
||||
positive form falls through cleanly.
|
||||
|
||||
Reachable in ordinary code: a table of `DATA` at the foot of a program is walked into by any
|
||||
routine above it that ends in a fall-through rather than an `END`, and a table of angles or
|
||||
offsets is exactly where negative numbers live. `tests/read_data.c`.
|
||||
|
||||
**A note on what this list is.** Items 1, 2, 6, 7 and 8 are defects by any reading. Item 3 is a
|
||||
design consequence nobody chose. Item 4 may be intended and is a documentation gap either way.
|
||||
Item 5 sharpens something already recorded. None of them is a complaint about the interpreter,
|
||||
which was a pleasure to write a real program against — they are the kind of thing one real
|
||||
program finds and a test suite written alongside the implementation structurally cannot,
|
||||
because the suite tests the constructs the implementer had in mind and a game reaches for
|
||||
whatever it needs.
|
||||
|
||||
Reference in New Issue
Block a user