Stop the listings and their chapters stating fixed defects as fact
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m30s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m27s

You were right that this was still everywhere. The two `.bas` files carried
their workarounds as present-tense statements about the interpreter -- "a name
first created inside a GOSUB costs a value slot that is never handed back",
"writing PAST the terminator of a short row draws nothing at all", "SSHAPE and
GSHAPE ignore the subscript on a string array", "a FOR inside a BEGIN block does
not survive the RETURN" -- and every one of those is now false. A comment that
lies is worse than no comment, and these were the first thing anybody opening
the files would read.

**The characters game takes the geometry fix.** `CW# = 16` becomes `CW# =
RGR(3)`, and the grid comes from `RWINDOW`, so the game fits whatever window and
font the host gives it. That was the one thing in the listing that would break on
a different font, and `RGR(3)` and `RWINDOW` were added to the interpreter
because of it -- leaving the constant in place while Chapter 17 teaches the
function was the inconsistency worth closing. `RGR(1)` still comes first so a
build with no graphics device refuses by naming the device that is missing.

Everything else in both listings keeps its shape and says why. Declaring every
name up front, guarding loops with `GOTO`, six scalars for six brick stamps: none
is forced any more and none costs anything, so they stay, with the comments
marking which rules stand and which are history. The sprites header's five traps
are marked FIXED where they are fixed.

The chapters follow: Chapter 17 Step 2 no longer says "the listing still has
`CW# = 16`", and Chapter 18's trap section is "five things that did not do what
they looked like" with the two that still stand named up front rather than left
to be counted.

Verified: Chapter 17 Step 2's block is once again verbatim from the listing, so
every quoted fragment in both chapters matches its source with no exceptions;
both games run 40 seconds on the SDL frontend with no error line; both suites
green in both configurations; `docs_examples` and `docs_screenshots --check`
pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 06:32:48 -04:00
parent 89fca8007b
commit 0679c3042b
6 changed files with 120 additions and 78 deletions

View File

@@ -54,10 +54,10 @@ with it. That is Chapter 18.
## Step 2: Measure the screen once, at the top ## Step 2: Measure the screen once, at the top
```basic norun ```basic norun
CW# = RGR(3)
CH# = RGR(4)
SCW# = RGR(1) SCW# = RGR(1)
SCH# = RGR(2) SCH# = RGR(2)
CW# = RGR(3)
CH# = RGR(4)
COLS# = RWINDOW(1) COLS# = RWINDOW(1)
ROWS# = RWINDOW(0) ROWS# = RWINDOW(0)
``` ```
@@ -68,10 +68,14 @@ columns and rows — see [Chapter 6](06-graphics.md#rgr). Everything below is de
these, including the ball's speed relative to the wall, so the game fits whatever window these, including the ball's speed relative to the wall, so the game fits whatever window
the host opened and whatever font it loaded. the host opened and whatever font it loaded.
**The listing in `examples/` still has `CW# = 16` written out**, measured by hand against **The first version of this file had `CW# = 16` written out**, measured by hand against
the bundled C64_Pro_Mono at 16 points, because when it was written a program had no way the bundled C64_Pro_Mono at 16 points, because there was no way to ask — and it was the
to ask — and it was the one thing in it that would break on a different font. That is one thing in the listing that would break on a different font. `RGR(3)` and `RWINDOW`
what `RGR(3)` and `RWINDOW` are for; `TODO.md` §6 item 31 is the history. were added to the interpreter because of it; `TODO.md` §6 item 31 is the history.
The order matters only for the error you get without a graphics device: `RGR(1)` refuses
first, naming the device that is missing, which is what should stop a game that cannot
draw.
Lay the rest of the geometry out in the same block — the wall in cells, the play area in Lay the rest of the geometry out in the same block — the wall in cells, the play area in
pixels: pixels:

View File

@@ -623,14 +623,15 @@ RETURN
A silenced voice costs nothing to issue. One line beats eleven scattered through the game. A silenced voice costs nothing to issue. One line beats eleven scattered through the game.
## Five things in this dialect that do not do what they look like ## Five things that did not do what they looked like
Each of these cost an evening, and each was filed in `TODO.md` §9 with a reduction and Each cost an evening, and each was filed in `TODO.md` §9 with a reduction and the file
the file and line of the cause. **Three of the five have since been fixed**, and are kept and line of the cause. **Three of the five have since been fixed** — the interpreter
here as the history of why this program is shaped the way it is — a listing whose changed, not the game — and they are kept here because the shape of the program is still
comments explain what it was avoiding is worth more than one quietly rewritten to pretend theirs: a listing whose comments explain what it was avoiding is worth more than one
the problem never existed. The two that remain are the first and the last, and the first quietly rewritten to pretend the problem never existed.
is the dangerous one.
**Two still stand, and they are the first two.** Read those; the rest is history.
### 1. The left operand decides integer or float arithmetic ### 1. The left operand decides integer or float arithmetic

View File

@@ -8,8 +8,8 @@ messages are characters in the text grid.
../../../build-akgl/basic breakout.bas ../../../build-akgl/basic breakout.bas
``` ```
It needs the SDL build. The stdio build has no graphics device, so `RGR` refuses on the It needs the SDL build. The stdio build has no graphics device, so the first `RGR` refuses
fourth line and the game stops there — which is exactly what it should do. and the game stops there — which is exactly what it should do.
| Key | Does | | Key | Does |
|---|---| |---|---|
@@ -38,11 +38,11 @@ a completely different program.
## Known warts, this game's own ## Known warts, this game's own
- **The cell size is a constant.** `CW#` and `CH#` are 16, measured with the bundled - **The cell size used to be a constant.** `CW#` and `CH#` were 16, measured by hand
C64_Pro_Mono at 16 points in the 800×600 window; the grid is 50×37. When this was against the bundled C64_Pro_Mono at 16 points, because BASIC had no way to ask — and it
written BASIC had no way to ask. It has now — `RGR(3)`, `RGR(4)` and `RWINDOW`, added was the one thing here that would break on a different font. `RGR(3)`, `RGR(4)` and
because of this listing and the listing has not been changed to use them, because `RWINDOW` were added to the interpreter because of this listing, and it uses them now,
what it demonstrates is what a program does when the interpreter will not tell it. so the game fits whatever window and font the host gives it.
- **Every character the game draws also goes to stdout**, because the frontend tees the - **Every character the game draws also goes to stdout**, because the frontend tees the
text sink to the terminal. It made the whole game auditable from a log file during text sink to the terminal. It made the whole game auditable from a log file during
development and it is pure noise the rest of the time. Redirect it. development and it is pure noise the rest of the time. Redirect it.
@@ -61,9 +61,12 @@ The interpreter defects this game turned up are filed in `TODO.md` §6 and §9,
reduction that fits on a screen. Most are now fixed — the value pool that killed the first reduction that fits on a screen. Most are now fixed — the value pool that killed the first
version after twenty-five seconds, the unreachable `WINDOW`, the character written past a version after twenty-five seconds, the unreachable `WINDOW`, the character written past a
short row — and the entries are struck rather than deleted, because the reduction is worth short row — and the entries are struck rather than deleted, because the reduction is worth
keeping. **The listing itself is unchanged**, and deliberately: it is what a program keeping.
written against those constraints looks like, and its comments explain what it was
working around. **The listing takes the geometry fix and leaves the rest of its shape alone.** Measuring
the cell instead of assuming it makes the game strictly better, so it does. Declaring
every name up front and looping with `GOTO` are no longer forced by anything, and are
still what the file does — its comments now say which rules stand and which are history.
## How this was checked ## How this was checked

View File

@@ -12,16 +12,23 @@ REM text grid and the sprites are both redrawn from persistent state every
REM frame. See README.md for the whole list of things that shaped this. REM frame. See README.md for the whole list of things that shaped this.
REM REM
REM Three rules this listing never breaks, each one earned the hard way: REM Three rules this listing never breaks, each one earned the hard way:
REM * Every name is created once, in the pool block below -- variables,
REM loop counters, all of it. A name first created inside a GOSUB or a
REM FOR costs a value slot that is never handed back, and there are 4096
REM of them for the whole run. A game that creates one name per tick is
REM dead in half a minute. This one creates none after startup.
REM * A row of text is written whole, from column 0. CHAR terminates the REM * A row of text is written whole, from column 0. CHAR terminates the
REM row where it stops, so writing at column N erases N+1 onwards -- and REM row where it stops, so writing at column N erases N+1 onwards.
REM writing PAST the terminator of a short row draws nothing at all.
REM * Mixed + and - are parenthesised. This parser reads a - b + c as REM * Mixed + and - are parenthesised. This parser reads a - b + c as
REM a - (b + c), and matches only ONE unparenthesised AND or OR. REM a - (b + c), and matches only ONE unparenthesised AND or OR.
REM * The main loop is a GOTO loop. Every loop and every GOSUB takes one
REM of 32 scopes, and a state change jumping out of a DO would leak one
REM every time; thirty-two lives later the program stops.
REM
REM Two more rules were true when this was written and are not any more.
REM They are recorded because the shape of the listing is still theirs:
REM * Every name is created once, in the pool block below. A name first
REM created inside a GOSUB or a FOR used to cost a value slot that was
REM never handed back, out of 4096 for the whole run, and this game died
REM after twenty-five seconds because of it. A scalar is free now; only
REM the DIMs below still have to be up here. TODO.md section 6 item 30.
REM * Writing PAST the terminator of a short row used to draw nothing at
REM all. The gap is padded with spaces now. TODO.md section 6 item 32.
REM REM
REM Written by Claude Opus (1M) REM Written by Claude Opus (1M)
REM #################################################################### REM ####################################################################
@@ -30,17 +37,23 @@ LABEL SETUP
SCNCLR SCNCLR
REM --- geometry ------------------------------------------------------- REM --- geometry -------------------------------------------------------
REM The cell is 16x16 and the grid is 50x37, measured with the bundled REM Nothing here is assumed. RGR(1) and RGR(2) are the window in pixels,
REM C64_Pro_Mono at 16 points in the 800x600 window. BASIC cannot ask for REM RGR(3) and RGR(4) are one character cell, and RWINDOW gives the text
REM the cell size -- WINDOW would tell us, but the standalone frontend's REM grid in columns and rows -- so the game fits whatever window the host
REM tee sink never offers it -- so these two are constants. Change them REM opened and whatever font it loaded. Everything below is derived from
REM together with the font or the window and everything else follows. REM these six, including the ball's speed relative to the wall.
CW# = 16 REM
CH# = 16 REM The first version of this file had CW# = 16 and CH# = 16 written out,
REM measured by hand against the bundled C64_Pro_Mono at 16 points, because
REM there was no way to ask -- and that was the one thing in the listing
REM that would break on a different font. RGR(3) and RWINDOW were added to
REM the interpreter because of it.
SCW# = RGR(1) SCW# = RGR(1)
SCH# = RGR(2) SCH# = RGR(2)
COLS# = SCW# / CW# CW# = RGR(3)
ROWS# = SCH# / CH# CH# = RGR(4)
COLS# = RWINDOW(1)
ROWS# = RWINDOW(0)
REM --- the brick wall, in cells --------------------------------------- REM --- the brick wall, in cells ---------------------------------------
BRW# = 4 BRW# = 4
@@ -60,18 +73,20 @@ TITROW# = 20
PSPD# = 10 PSPD# = 10
REM --- pools ---------------------------------------------------------- REM --- pools ----------------------------------------------------------
REM Every name in the program is created HERE, and this is load-bearing REM Every name in the program is created HERE.
REM twice over.
REM REM
REM A GOSUB gets its own scope. Assignment walks up the chain and finds an REM One reason still stands. A GOSUB gets its own scope: assignment walks up
REM outer variable, but a name first seen inside a subroutine is created in REM the chain and finds an outer variable, but a name first seen inside a
REM the subroutine and dies at RETURN -- so HIT# and BI# would mean nothing REM subroutine is created in the subroutine and dies at RETURN -- so HIT#
REM to the caller if they were not already out here. REM and BI# would mean nothing to the caller if they were not already out
REM here, and HITTEST would answer into two variables nobody can read.
REM REM
REM And a name that is created costs a value slot for good. The pool is a REM The other has been fixed. A scalar used to cost a value slot for good --
REM bump allocator with no free (../akbasic/src/value.c:142) and recycling a REM the pool is a bump allocator with no free, and recycling a variable took
REM variable takes fresh slots, so 4096 creations ends the run whenever they REM fresh slots, so 4096 creations ended the run whenever they happened. A
REM happen. Declared once, the whole game then runs on nothing but stores. REM scalar now lives in the variable itself and costs the pool nothing; only
REM the DIMs still spend, which is why they are the part that has to be up
REM here. The rest of this block is habit, and harmless.
DIM BR#(60) DIM BR#(60)
DIM SB#(63) DIM SB#(63)
DIM SP#(63) DIM SP#(63)

View File

@@ -2,8 +2,11 @@
Breakout, written entirely in BASIC for the AKGL BASIC interpreter, using downloaded CC0 Breakout, written entirely in BASIC for the AKGL BASIC interpreter, using downloaded CC0
sprite artwork for the paddles, the balls and the powerup gems. The bricks, the HUD and sprite artwork for the paddles, the balls and the powerup gems. The bricks, the HUD and
the lettering are drawn, captured with `SSHAPE` and installed as sprites — which is the the lettering are drawn, captured with `SSHAPE` and installed as sprites — because a
only way a program in this interpreter can put a picture on the screen. sprite is the one thing this interpreter puts on the screen for nothing. The text layer
owns every row of the window unless a program takes some back with `WINDOW`, and even
then a drawing has to be re-issued every frame; a sprite is drawn from state the
interpreter keeps.
```sh ```sh
../../../build-akgl/basic breakout.bas ../../../build-akgl/basic breakout.bas
@@ -89,6 +92,8 @@ drawing. The two that stand are the left-operand arithmetic rule, which turned o
decision rather than a defect and is now documented as one, and the 256-line batch tear, decision rather than a defect and is now documented as one, and the 256-line batch tear,
which belongs to the host. which belongs to the host.
**The listing is unchanged**, and deliberately: it is what a program written against those **The listing's code is unchanged**, and deliberately: it is what a program written
constraints looks like, and its comments explain what it was working around. Six separate against those constraints looks like, and none of the workarounds costs anything now that
scalars for six brick stamps is no longer necessary, and it is still what the file does. they are not needed. Six separate scalars for six brick stamps, and loops guarded by
`GOTO` rather than wrapped in a block, are both still what the file does. Its comments say
which of the five traps stand and which are marked FIXED.

View File

@@ -7,12 +7,18 @@ REM Five things about this interpreter shape everything below, and they
REM are worth knowing before the first line stops making sense. REM are worth knowing before the first line stops making sense.
REM REM
REM 1. The text layer repaints every row of the window opaque black after REM 1. The text layer repaints every row of the window opaque black after
REM the program's steps have run, so anything DRAW or BOX put on the REM the program's steps have run, so by default anything DRAW or BOX
REM screen is wiped before it is ever presented. Sprites are drawn REM put on the screen is wiped before it is ever presented. Sprites are
REM after the text layer, so a sprite is the only thing that can be REM drawn after the text layer, so a sprite is the one thing that is
REM seen. Everything drawn here is therefore captured with SSHAPE and REM seen for nothing. Everything drawn here is therefore captured with
REM installed as a sprite with SPRSAV -- that is what sprites 1 and 2 REM SSHAPE and installed as a sprite with SPRSAV -- that is what
REM are. They are the screen. REM sprites 1 and 2 are. They are the screen.
REM
REM (WINDOW can shrink the text area and hand the rest of the window to
REM the drawing verbs; it could not be reached from this build when
REM this was written. It would not help here anyway: a drawing has to
REM be re-issued every frame and fit inside one batch, and a sprite has
REM neither constraint.)
REM REM
REM 2. The host runs 256 source lines and then presents the frame, and a REM 2. The host runs 256 source lines and then presents the frame, and a
REM present throws the drawing buffer away. So all the drawing between REM present throws the drawing buffer away. So all the drawing between
@@ -33,9 +39,10 @@ REM enough to both that a constant is spelled out rather than named
REM and several routines that would read better as subroutines are REM and several routines that would read better as subroutines are
REM BEGIN blocks instead. REM BEGIN blocks instead.
REM REM
REM 5. Five things in this dialect do not do what they look like they do, REM 5. Five things in this dialect did not do what they looked like they
REM and each one cost an evening. They are noted where they bite, and REM did, and each one cost an evening. THREE OF THEM HAVE SINCE BEEN
REM collected here so nobody has to find them twice: REM FIXED, and are marked below; the shape of this listing is still
REM theirs, which is why they are still written down:
REM REM
REM * **The left operand decides whether an expression is done in REM * **The left operand decides whether an expression is done in
REM integers or in floats.** 0 - V% throws V%'s fraction away and REM integers or in floats.** 0 - V% throws V%'s fraction away and
@@ -48,12 +55,16 @@ REM result somewhere with a % on it.
REM * A FOR whose bounds are equal does not run its body at all, so REM * A FOR whose bounds are equal does not run its body at all, so
REM every loop over a list has the one-item case written out beside REM every loop over a list has the one-item case written out beside
REM it. REM it.
REM * A FOR inside a BEGIN block does not survive the RETURN of the REM * FIXED. A FOR inside a BEGIN block that was SKIPPED did not
REM routine holding it; the interpreter loses the GOSUB. Loops in REM survive the RETURN of the routine holding it; the interpreter
REM this program are guarded by GOTO rather than wrapped in a block. REM lost the GOSUB and reported "RETURN outside the context of
REM * SSHAPE and GSHAPE ignore the subscript on a string array, so REM GOSUB". Loops in this program are guarded by GOTO rather than
REM every element resolves to element zero. The six brick stamps are REM wrapped in a block, and are left that way because it costs
REM therefore six separate scalars. REM nothing. TODO.md section 9 item 2.
REM * FIXED. SSHAPE and GSHAPE ignored the subscript on a string array,
REM so every element resolved to element zero. The six brick stamps
REM are six separate scalars for that reason; an array works now.
REM TODO.md section 9 item 6.
REM * READ walks one cursor through every DATA item in the file in the REM * READ walks one cursor through every DATA item in the file in the
REM order they were written, no matter which routine is reading. REM order they were written, no matter which routine is reading.
REM REM
@@ -140,10 +151,11 @@ REM ------------------------------------------------------- shapes and dirt
REM SHN# counts the SSHAPE slots spent. There are sixteen, nothing gives REM SHN# counts the SSHAPE slots spent. There are sixteen, nothing gives
REM one back, and GRAPHIC 5 gives back all of them at once -- so the brick REM one back, and GRAPHIC 5 gives back all of them at once -- so the brick
REM stamps are thrown away and rebuilt every time the pool runs dry. REM stamps are thrown away and rebuilt every time the pool runs dry.
REM One scalar per row colour, and scalars rather than an array on REM One scalar per row colour, and scalars rather than an array because
REM purpose: SSHAPE and GSHAPE read the leaf they are handed without REM SSHAPE and GSHAPE used to read the leaf they were handed without
REM applying its subscript, so a string array element always resolves to REM applying its subscript -- a string array element always resolved to
REM element zero and every brick comes out the same colour. REM element zero, and every brick came out the same colour. That is fixed
REM and an array would work now; six scalars is what this listing has.
S0$ = "" S0$ = ""
S1$ = "" S1$ = ""
S2$ = "" S2$ = ""
@@ -330,9 +342,10 @@ REM
REM Rebuilding the brick stamps comes first, because everything else REM Rebuilding the brick stamps comes first, because everything else
REM draws with them and GRAPHIC 5 has just thrown them away. REM draws with them and GRAPHIC 5 has just thrown them away.
REM ===================================================================== REM =====================================================================
REM Labels rather than BEGIN blocks here, and it is not a style choice: a REM Labels rather than BEGIN blocks here. When this was written it was not
REM RETURN inside a block leaves the interpreter with no GOSUB to return REM a style choice -- a loop inside a block that was skipped left the
REM from and stops the program. REM interpreter with no GOSUB to return from and stopped the program. That
REM is fixed; the shape is kept because it reads the same either way.
LABEL DRAWJOB LABEL DRAWJOB
IF SHN# < 14 THEN GOTO DRAWJOB2 IF SHN# < 14 THEN GOTO DRAWJOB2
GOSUB DRAWPROTOS GOSUB DRAWPROTOS
@@ -399,8 +412,9 @@ Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW
Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW
Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW
Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW
REM A FOR inside a BEGIN block does not survive the RETURN at the end of REM A skipped BEGIN block holding a FOR used to break the RETURN at the end
REM the routine that holds it, so this loop is guarded by a GOTO instead. REM of the routine holding it, so this loop is guarded by a GOTO instead.
REM Fixed since; left alone because a GOTO guard costs nothing.
IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0) IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0)
IF PN# < 2 THEN GOTO PLDONE IF PN# < 2 THEN GOTO PLDONE
FOR I# = 0 TO PN# - 1 FOR I# = 0 TO PN# - 1