Commit Graph

5 Commits

Author SHA1 Message Date
91fadf032b Bring the tutorials back in line with the fixed interpreter
Eleven of the thirteen defects these two games found are fixed, and the
chapters that taught around them said things that are no longer true.

Chapter 17: the drawing verbs are no longer invisible, they are covered by a
text layer that `WINDOW` can now shrink; the cell size is `RGR(3)` and the grid
is `RWINDOW`, not a hand-measured constant; a character written past a short
row's end lands. Chapter 18: traps 3 and 4 -- the skipped block that broke its
caller's `RETURN`, and `SSHAPE` ignoring a subscript -- become history rather
than warnings, and Step 3 no longer claims a sprite is the *only* way to put a
picture up, only the one that costs nothing.

**Neither `.bas` listing changes, and both READMEs now say why.** The character
game still writes `CW# = 16` and the artwork game still keeps six brick stamps
in six scalars. What those listings are worth is being what a program written
against those constraints looks like, with comments explaining what each one was
avoiding -- rewriting them to pretend the problems never existed would throw
that away. So Chapter 17 Step 2 shows the `RGR(3)`/`RWINDOW` form and says
plainly that the listing beside it does not use it. That is the one place in
either chapter where a quoted fragment is not verbatim from the game.

Also: the closing pointers now say which entries are struck and which stand, and
the budgets table in Chapter 18 notes that several of those budgets were tighter
when the game was written.

TODO.md section 9 item 3 is updated rather than struck: the `WINDOW` half is
fixed, the default text area owning the whole window is not, and whether that
default is right is a question rather than a defect.

Verified against the fixed interpreter: both games run 45 seconds on the SDL
frontend with no error line and the score climbing; both suites are green in
both configurations; `docs_examples` passes and `docs_screenshots --check`
re-renders all thirteen figures byte-identically; coverage is 95.0% of lines
against the 90 gate, with src/variable.c at 100%; and every other quoted
fragment still appears verbatim in the listing it came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:31:06 -04:00
8594d8471d Honour a subscript in SSHAPE and GSHAPE
`shape_variable()` took the identifier off the leaf and looked the variable up
without ever evaluating the subscript, and both verbs then addressed element
zero with a literal. So `SSHAPE SH$(2), ...` wrote the handle into `SH$(0)` and
`GSHAPE SH$(2)` stamped whatever was in `SH$(0)`.

Ordinary assignment and `PRINT` honour the subscript, which is what made this
expensive: a program keeping several saved shapes in an array got every one of
them resolving to the same element, silently, and the only symptom was that
every stamp came out as the last shape captured. The Breakout in examples/ keeps
its six brick stamps in six separate scalars for exactly this reason.

`SPRSAV` was the counter-example and is the model -- it evaluates its argument
and handles an array element correctly. The subscript resolution itself is now
shared: `collect_subscripts()` comes out of src/environment.c as
`akbasic_environment_collect_subscripts()`, so a verb taking a variable by name
resolves a subscript the same way assignment does rather than each verb deciding
for itself.

tests/graphics_verbs.c covers TODO.md's reduction -- which used to print
"[SHAPE:0] []" and now prints "[] [SHAPE:0]" -- and the case a program actually
wants: two shapes captured into two elements, each stamped back through its own,
asserted against the device log so a fix that merely made the strings look right
would not pass.

Chapter 18's trap 4 becomes history, and Chapter 6 says an array works.

TODO.md section 9 item 6, struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:10:24 -04:00
5d33237eed Release the scope a skipped BEGIN block's loop pushed
`akbasic_parse_for()` and `akbasic_parse_do()` create their environment while
the line is *parsed*; whether to skip it is decided afterwards, when the line is
evaluated. So a loop inside a block that was not taken pushed a scope, its body
was skipped, and the `NEXT` or `LOOP` that would have popped it was skipped too.
Nothing else ever would.

At the top level that exhausted the pool after thirty-two skips. Inside a
routine it was far more confusing: the orphan sat between the routine and its
caller, so the `RETURN` after the block reported "RETURN outside the context of
GOSUB" from a routine that plainly *was* entered by a `GOSUB` -- naming the one
construct that was not at fault, which is why it cost an evening to find.

The skip now releases what parsing pushed.

**Narrower than it first looks.** Releasing on any skip breaks
tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas: a
zero-iteration `FOR` skips its body by the same mechanism, and there the orphan
is load-bearing -- it absorbs the inner `NEXT` so the outer `NEXT` still finds
its own `FOR`. Releasing it turns that case into "NEXT outside the context of
FOR". So the release is conditional on the skip being a *block* skip, which is
decidable because nothing inside a skipped block ever runs to arm a `NEXT` wait.
Both halves are asserted side by side in tests/structure_verbs.c, the second one
citing the golden case that caught it.

The forty-skip case names its own step budget: a skipped line is not free, and
forty passes over a five-line block cost about 2700 steps against the shared
runner's 2000.

Chapter 18's trap 3 becomes history rather than a warning, and the note in Step
5 that called `GOTO`-guarded loops "not a style choice" now says why the shape
is kept anyway.

TODO.md section 9 item 2, struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:55:10 -04:00
05f241aca1 Stop a scalar created inside a scope costing value-pool slots
A scalar now lives in the variable record (`akbasic_Variable::inlinevalue`)
rather than drawing from the value pool, so a `GOSUB` local, a `FOR` counter and
a `DEF` parameter cost nothing at all.

The pool is a bump allocator with no free, and its comment justified that with
"nothing in BASIC destroys a variable". Scope exit does: it marks the variable
slot unused, `new_variable()` memsets the slot it hands back -- clearing
`values` -- and `variable_init()` therefore took *fresh* slots for a variable
whose old ones were still counted. Every scope that created a local leaked, with
no diagnostic until the pool ran dry on whichever line happened to be unlucky.

Six thousand `GOSUB`s creating one local used to die on the 4091st at `LOC# = 1`
with "Array of 1 elements does not fit in the 0 remaining value slots". They now
run. A `DEF` called eight thousand times used to die between the four and five
thousandth -- the leaking slot was the call scope's parameter, which is a scalar
-- and both forms now run. A game creating one name per tick was dead in half a
minute; the Breakout in examples/ was, after twenty-five seconds.

**A `@` name is the one exclusion, and it is the whole of it.** A structure or a
pointer to one keeps pool storage, because a pointer into a record outlives the
scope that DIMmed it -- docs/16-structures.md says nothing is reclaimed and
`prev_environment()` relies on it. The name suffix is the right test rather than
`structtype`, which the DIM path sets *after* calling `variable_init()`. A local
array therefore still leaks, deliberately, and is now the narrow rule the
tutorial teaches.

`SWAP` needed the other half: it copies whole variable records, so the `values`
pointer that came over named the other variable's inline slot -- which by then
held this variable's own old value -- and SWAP silently did nothing. Caught by
tests/language/housekeeping/verbs.bas, which is the golden corpus earning its
keep.

tests/value_pool.c is the new coverage. It asserts the mechanism as well as the
consequence: a later change that moved arrays inline too would pass every
behavioural case and quietly break the pointer guarantee. The sharpest case
takes the pool's whole 4096 slots in four arrays after two hundred scope
entries, so one leaked slot has nowhere to go.

Chapter 17 Step 3 taught "declare every name at the top" and no longer needs to.
It now teaches what is still true -- a name first seen inside a subroutine dies
at RETURN, so a routine cannot answer its caller through one -- and its
demonstration is the array case, which still fails.

TODO.md section 6 item 30 and section 9 item 1, both struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:47:49 -04:00
cb0e2d0800 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
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>
2026-08-01 22:54:42 -04:00