0d79a3f52cfe0554738de6ba60ea195c1b0ff7c4
35 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
f8fc086df3
|
Stop chapter 18 contradicting itself about FILLROW and DRAWHUD
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m23s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m29s
The assembly step still carried the pre-conversion FILLROW, without the SOLID registration Step 8 spends a section teaching -- a reader following the assembly order got a wall the ball passes straight through. DRAWHUD cleared the graphics layer and never stamped the HBL$ eraser Step 6 tells you to make. Both together took the field down every time a digit in the HUD changed. It is GRAPHIC 1, 0 and a GSHAPE of HBL$ at 0, 0, and the difference between that and the routines that draw the field is now stated rather than left to be discovered. Also say where HUDTEXT is called from, which nothing did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
851b03bcf6
|
Give chapter 18 the six pieces a reader had to invent
A reader building the game from the chapter alone had to guess at all of these, and three of them are code the chapter describes and never shows: - the HUD eraser stamp, promised in Step 6 as "make a second one while you are here" and then never given - the gem roll at the end of ONEBRICK, without which nothing ever drops - BX1# and BY1#, which SPAWNGEM reads and only ONEBRICK can set And three rules the chapter leans on without stating: - MID and INSTR count from zero, which is what makes a glyph number an array index with no adjustment - sprite n sets bit 2^(n-1), which is where 16, 32 and 64 come from - states 5 and 6 have no tick routine on purpose; that is what a pause is Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
fa5623b722
|
Say where the collision handler is armed, and what BYE's last two verbs do
A reader building chapter 17 from the text alone had to infer that COLLISION 2, BRICKHIT belongs in the setup block rather than the frame loop, and met SCNCLR and QUIT for the first time in the assembly step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
a01554d304
|
Teach the artwork tutorial the converted listing
Chapter 18 still taught the machinery the listing lost when it moved onto SOLID and the persistent drawing layer: the frame-boundary PACE routine, the flattened live list, the SSHAPE pool accounting, and the sixty lines of BALLBRICKS/TESTCELL that computed a minimum translation axis by hand. Step 8 now registers each brick with SOLID, arms COLLISION 2 and reads the contact back with RCOLLISION, including the point that costs an evening if it is missed: the BUMP mask is by sprite, and the balls are sprites 5, 6 and 7, so their bits are 16, 32 and 64. The FOR-with-equal-bounds rule that every one-item case in the chapter depends on is now stated where it is first used rather than referred to from a step that no longer exists. The listing loses six declarations orphaned by the conversion and the comment blocks that still described the captures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
8da8d717c8
|
Teach chapter 17 the collision verbs its listing now uses
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m42s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m29s
The previous commit converted `examples/breakout/characters/breakout.bas` and left the chapter teaching three routines the listing no longer has. This closes that. Step 5 gains the `SOLID` registration beside the `BR#()` array, and says why there are two records of the same thing: one is the program's, one is the interpreter's, and the id is the array index plus one so neither has to be looked up from the other. It is also now the only place cells and pixels meet, which Step 2 used to promise about Step 10. Step 10 is rewritten. It was `HITTEST` -- fourteen lines dividing pixels by cell sizes to recover a grid index -- plus `XBRICK` and `YBRICK` testing the ball's leading edge, with a paragraph explaining that the cost of testing an edge rather than a box is missing a brick clipped at the corner by up to seven pixels. All of that is gone. What replaces it asks four questions: which rectangle, is it still there, which way out and how far, and which axis to reverse. Three things in it are worth the reader's attention and get it: `BUMP(2)` is a separate accumulator from `BUMP(1)` so the handler never sees the paddle; the guard against being told about an already-broken brick, because a handler runs a line or two after the overlap; and `SOLID BI# + 1` in `KILLBR`, without which the ball goes on bouncing off a brick that is no longer drawn. That last one is now a row in the rules table. The float note is kept and sharpened rather than dropped. `D%` must be a float because field 4 is one, and the products land in integer `BX#`/`BY#` -- which is safe here because a box against a box gives a normal that is exactly -1, 0 or 1, and would *not* be safe against a circle. Stated as a reason rather than left as something that happens to work. Step 9 loses its two brick calls and its `OX#`/`OY#` backup, with a sentence saying why there is nothing to restore to. Step 16's declaration block and name count follow, and the skeleton arms the handler. Every fenced example still runs; both suites green; the game runs clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
a9600c3fcc
|
Say what was hit, which way is out, and how far
The narrowphase has been producing a contact since it went in, and the interpreter was throwing it away. `RCOLLISION(n, f)` reports it: what was hit (a sprite or a `SOLID` rectangle), which one, the contact normal, the penetration depth, the contact point, and which axis to reverse. **The normal points out of the other thing and toward this one**, so a program moves along it by the depth and is exactly clear. That sign is the one assertion in the new test that could not be caught any other way -- both parties of a sprite-against-sprite hit get their own record, each pointing the way *that* sprite has to move, and sharing one would tell both to go the same direction, which is how two things end up stuck inside each other. **Field 7 is the one that deletes the most BASIC.** It is the minimum translation axis, computed from the normal in C, and it is there because doing it in BASIC means comparing two floats -- which is exactly where this dialect's left-operand rule catches people. `BALLBRICKS`/`TESTCELL` in the artwork breakout spend six lines computing an overlap rectangle and comparing its width to its height to get this number. The record is **sticky and deepest-wins**: replaced whenever that sprite is in a contact and otherwise left alone, so `BUMP` stays the event and this stays the detail of it. Making it clear itself when nothing touches would break the pairing, because `BUMP` accumulates across steps and a once-a-frame poll would find the detail already gone. Reading `BUMP` clears both, so they cannot disagree. Deliberately narrower than `akgl_Contact`: no actor pointers, because BASIC has no actor; no tile fields, because there is no tilemap; no z, because every test is planar; and **no `dt` and no `sensor`**, which libakgl documents as filled in by the resolver. This interpreter never resolves anything, so those two come back zero and mean nothing, and an always-zero field in a reference table is a lie. Documented with the two caveats that matter: fields 2, 3 and 4 are floats and want a `%` variable, and the contact *point* is exact only for two boxes -- libakgl's solver returns a point on the portal it converged to, while the normal and depth are exact for every pair. Chapter 8's collision section stops claiming only type 1 exists, which has been false since the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
5709dc160c
|
Keep what a program draws, instead of making it a sprite
A drawing lasted exactly one frame. The verbs are immediate, they went to the back buffer, SDL double-buffers and the frontend never clears -- so the only way to keep a picture was to capture it with `SSHAPE` and install it as a sprite, which is what `examples/breakout/sprites/breakout.bas` spends two of its eight sprites doing. That was TODO.md section 9 item 9. The drawing verbs now render into a layer texture the frame composites under the text and the sprites. Draw once; it is there on every frame after. **Bracketed around the step phase, not around each verb.** One pair of `SDL_SetRenderTarget` calls a frame instead of one per `DRAW`, and it is also what makes `SSHAPE` read back what the program has just drawn rather than whatever the last frame left. **The layer is transparent where nothing was drawn.** It covers the whole window and composites underneath, so an opaque one would black out the frame the moment a program issued a single `DRAW`. And a fresh SDL target texture's contents are undefined, so it is cleared on creation -- skipping that puts uninitialised memory under the first frame's text and looks like a driver bug rather than a missing memset. **The line editor forced a wrinkle worth naming.** `akbasic_frontend_akgl_pump()` is called from two places with different answers to "is a render target current": the frame loop calls it between steps, and the sink's editor calls it from *inside* a step, borrowing a frame while it waits for a typed line. SDL refuses to present while a target is current, so the pump ends the layer, presents, and puts it back only if it was the one that ended it. `akgl_frontend` caught this -- it drives a REPL session, and it failed with "You can't present on a render target" the first time the brackets went in. This does not make a drawing *visible* on its own. The text layer still repaints every row it owns, opaque, every frame, and by default it owns the whole window; `WINDOW` shrinks it and that half was already fixed. The two together are what a picture needed, and the tests assert both -- a pixel still there a frame later with nothing redrawn, and a pixel below a shrunk text area surviving the text repaint. The second assertion wipes to a non-black colour first, because against black it could not tell a transparent layer from an opaque one. The tests found two of their own bugs on the way: `stop_runtime()` was not tearing the graphics backend down, so re-initialising it dropped a live texture on the floor; and a first draft called `begin()` before `start_runtime()`, which re-inits the backend, so the assertion read back off an orphaned render target and passed while proving nothing. Chapters 6 and 13 stop saying a drawing has to be redrawn every frame, because it does not. The batch-boundary tear stays documented -- it bites an `SSHAPE` capture, which matters much less now that capturing is not the only way to keep a picture. Both games still run clean. 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
802bbcc17a
|
Collide sprites with rectangles that are not sprites
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m19s
akbasic CI Build / sanitizers (push) Failing after 4m33s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m29s
`SOLID id, x1, y1, x2, y2` registers static collision geometry; `SOLID id` retires one and a bare `SOLID` retires them all, the way `TRAP`, `COLLISION` and `DCLOSE` all read absence. `COLLISION 2` and `BUMP(2)` stop being refused and mean *sprite met static geometry*. **This is the thing eight sprite slots made impossible.** A wall of bricks wants sixty, so until now a program could only collide with one by doing the arithmetic itself against its own array -- which is exactly what both breakout listings do, at about two hundred lines between them. A rectangle costs no sprite slot. The id is the **program's own number**, 1 to 64, not a minted handle. That is the whole trick for "which brick did I hit": the id comes back out again, so a wall built as `SOLID I#, ...` maps onto `B#(I#)` with no lookup, and retiring a broken brick is `SOLID I#`. `COLLISION 2` was refused with "sprite-to-background collision needs the screen read back every frame", which was true of the question a C128 asks -- a sprite against the bitmap's set pixels. `SOLID` gives this interpreter a background made of rectangles instead, which is the same question in a form it can answer. Same move `SPRSAV` made when it learned to take an image path. `AKBASIC_INTERRUPT_BACKGROUND` has been sitting in the interrupt table commented "COLLISION 2 -- sprite met background; refused" the whole time. Its accumulator is separate, so a sprite hitting a wall never sets a bit in `BUMP(1)`. **There is no `akgl_CollisionWorld` here, and that is deliberate.** libakgl's uniform grid keeps its cell heads, cell size and origin in file-scope statics, so it is one index per process -- and `akgl_collision_world_init()` ends in a `reset()` that memsets those heads *and* calls `akgl_heap_init_collision_cells()`. An interpreter embedded in a game with its own collision world would have destroyed every registration that game had made, on the first `SOLID` a script ran. So the geometry is indexed by an ordinary array here and pairs go straight to `akgl_collision_test()`, which needs no world. At sixty-four rectangles that is the right answer anyway; libakgl's own numbers put a naive sweep at 0.7% of a frame at sixty-four objects. **The scan now short-circuits when nothing has moved**, and that is what makes any of it affordable. Its inputs are the sprites' boxes, which slots are collidable, and the static geometry; if none changed the answer cannot have. A frame runs one full scan and 255 cached ones. Eight sprites against sixty-four rectangles is five hundred and twelve tests -- fine once a frame, ruinous 256 times. The benchmark was rewritten to say which path it is timing, because with the cache in place a loop that only calls the scan measures the short circuit and nothing else. Breakout now costs 590.6 ns for its one full scan plus 255 cached at 40.0, which is 10.8 us against a 1.19 ms frame -- **0.91%, less than the 2.0% it cost before any of this work**, with static geometry and contacts added on top. `NEW` retires the rectangles, where it cannot undefine a sprite pattern: there *is* an entry point for this one, so leaving them would be a choice, and the wrong one -- a rectangle is invisible, so one left behind by a deleted program is an unexplainable collision in the next. `CLR` leaves them alone. `tests/sprite_verbs.c` gains the whole second path against the mock and its `COLLISION 2` case is rewritten: it pinned the refusal, and now pins that type 2 arms its own handler without disturbing type 1's. `tests/akgl_backends.c` gains the end-to-end version, including a full sixty-four-rectangle wall so the proxy budget is exercised at its ceiling and the pool has to come back intact, and the sixty-fifth refused by name. A bare `SOLID` needed `akbasic_parse_optional_arglist` rather than `akbasic_parse_arglist`, which `DCLOSE` already uses for the same shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
f005b88980
|
Let a program say what part of a sprite collides
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m20s
akbasic CI Build / sanitizers (push) Failing after 4m31s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m28s
`SPRHIT n, kind [,x1, y1, x2, y2]` gives a sprite a collision shape: a box, a circle inscribed in it, or a capsule. `RSPHIT(n, f)` reads it back in SPRHIT's own argument order, the way RSPRITE and RSPPOS already do, and needs no device because it answers from interpreter state. The rectangle is two corners measured from the sprite's top-left, in device pixels -- the same `x1, y1, x2, y2` that `BOX` and `SSHAPE` take. A dialect with two spellings for a rectangle is one nobody can write from memory. Omit it and the shape fits whatever the picture turned out to be, which is what a sprite loaded from a file needs: `SPRSAV "ship.png", 1` takes the image's own size and the program never learns what that was. **A sprite nobody has shaped collides with its whole frame, expansion bits included, exactly as before.** That is a promise rather than a convenience, and it has its own test: the same two sprites in the same two places, once with no SPRHIT and once with a four-pixel box, reporting a collision and then not. Named SPRHIT rather than SPRSHAPE because "shape" already means "a region SSHAPE saved" in this dialect, in this very chapter -- `SPRSAV A$, 1` takes one -- and a reader who typed `SPRSHAPE A$, 1` would have had every reason to. Both names, and RSPHIT, were grepped against every label in docs/, examples/ and both corpora first: a bare word is a label here, so a verb and a label share one namespace and taking a name a checked-in listing already uses would break it silently. `SPRHIT n, 0` takes a sprite out of collision while leaving it on the screen -- the ghost, the flashing invulnerable player, the pickup already taken. Hiding it with `SPRITE n, 0` stops it colliding too, and is what you want when it should not be seen either. The circle answers the complaint chapter 8 already ships a figure of. That figure shows two discs whose *boxes* touch at a corner while the artwork is nowhere near, and `BUMP(1)` reporting a collision; two `SPRHIT n, 2` and it stops. The test asserts both halves so the figure's caption stays true. `tests/verbs_table.c` caught RSPHIT filed after RSPPOS rather than before it, which is the sorted-table test doing exactly the job it exists for. Docs: a new section in chapter 8, rows in the verb and function references in alphabetical order, and chapter 13's "collision is by bounding box" becomes "collision is by shape" with the addition named. 111 with akgl, 110 without, and the artwork breakout still runs clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
1da37cf374
|
Take libakgl 0.8.0, and correct the status range it grew
The pin moves e4aa6a5 -> 149bee0, which is libakgl 0.8.0. Nothing in this repository changed to accommodate it: both suites pass unmodified, 110 without akgl and 111 with, and both breakout games run forty seconds headless with no error line. **0.8.0 is a whole collision subsystem** -- shapes, pooled proxies, a pluggable broad phase over a uniform grid, a narrowphase answering with a contact carrying a normal and a depth, world box queries, and static proxies for geometry that is not an actor. None of it is called from here yet. This commit is the pull and nothing else, so that if it has to come out it is one revert of one commit. **It adds two submodules of its own**, `deps/libccd` and `deps/tg`, so a tree that updates without `--recursive` configures and then fails compiling libccd. libakgl's own suite was built and run standalone at RelWithDebInfo before akbasic was pointed at it: 33/33. `libakstdlib` and `libakerror` did not move. MAINTENANCE.md requires checking, and the answer this time is that libakgl 0.8.0 pins exactly what this repository already pins -- 669b2b3 and 5eaa956 -- so the pairing rule is satisfied without a bump. Recorded because "we checked and it was already aligned" and "we forgot to check" look identical in a diff. **The version floor moves to 0.8.0** with its paragraph, following the convention in that header of saying what each minor release did and why the floor moved anyway. Worth knowing for whoever reads it next: `akgl_Actor` grew fields, so a translation unit compiled against a 0.7 `actor.h` and linked against 0.8 writes `renderfunc` and `actorData` at the wrong offsets -- and `src/sprite_akgl.c` writes exactly those two. That is the case the soname cannot catch and the guard exists for. **libakgl's reserved status band grew from five codes to six**, gaining `AKGL_ERR_COLLISION` at `AKGL_ERR_BASE + 5`, so it now owns 256-261 and the headroom below akbasic's band starts at 262. Four documents said otherwise: the coordinated range map in MAINTENANCE.md, the comment above the enum in `include/akbasic/error.h`, the file header of `include/akbasic/akgl.h`, and chapter 15. That map is the only coordination there is -- nothing enforces a band boundary at compile time, and the first anybody would know of an overlap is a status printing under the wrong owner's name in a stack trace. Also cleaned on the way past: `deps/libakgl/deps/libakstdlib` was showing dirty in `git status`. It had no local edits -- the checkout was simply one commit behind the gitlink libakgl records -- so `git submodule update` restored it and nothing was lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
1fb808f480
|
Rewrite the two game tutorials as instructions rather than commentary
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m37s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m30s
Chapters 17 and 18 read as a code review of a finished listing: they explained why each decision had been made, walked through the project's own history, and led with what had once been broken. A reader who wanted to build the game got the reasoning and had to reconstruct the program. Both are now step-by-step. Each opens with a picture of the finished game and a bullet list of the steps, each bullet is a section, and each section states its goal, shows the code, and says how to check it. Chapter 17 is sixteen steps and Chapter 18 is thirteen, and the last of each is the assembly: the order of the file, the full declaration block, and the routines the earlier steps referred to. Project history is gone -- it belongs in Chapter 14 and in git -- and where a listing has to do something awkward, the tutorial shows how first and names the `TODO.md` item that will make it unnecessary second. **Three defects had no entry anywhere**, which the rewrite found by trying to state each rule as a rule. §6 item 35: `a - b + c` computes `a - (b + c)`, because `subtraction()` sits above `addition()` as its own precedence level and the inner loop eats the `+`. Item 36: only one unparenthesised `AND` or `OR` is matched, which is item 12's `if`-where-`while` on the one operator pair item 12 did not reach. Item 37: a `GOTO` out of a `FOR` or a `DO` leaks the loop's scope, so a main loop written that way stops on the thirty-second lost life -- which is why both games are built out of `LABEL` and `GOTO`, and it is a workaround rather than a preference. Chapter 3 gains the identifier rule the third trial ran into: there is no underscore in a name, and the error says `UNKNOWN TOKEN _`. **`tools/screenshot.c` learned to draw the text layer**, behind a new `text=1` fence attribute, because Chapter 17's game is characters in the grid and a figure without that layer is two sprites on black. It opens the bundled font at the size the standalone frontend uses, so a figure's cell size is the reader's cell size, and it uses the akgl sink alone rather than a tee so the program's output lands in the picture instead of on the stdout the caller reads to decide a figure failed. Both new figures -- `breakout-game.png` and `breakout-game-artwork.png` -- are generated from listings in the chapters like every other one. Verified by handing each chapter, alone, to an agent on a much smaller model and telling it to build the game from the tutorial text with the `examples/` tree off limits. The first pass scored 3.5 and 3 out of 10 and named what was missing: routines referred to but never shown, the third level layout, the sprite `DATA`, the font table, edits to earlier routines that were never marked as edits. Those are now in. The second pass built a 658-line Chapter 17 game that plays itself for ninety seconds with the score at 1890 and no error line, and the third built a 1053-line Chapter 18 game with 61 labels, no invented routines, no gaps found, and forty seconds clean. 9/10 and 8/10. One real bug in the new prose, caught in review: Chapter 18's `HITBAR` did not set the `HIT#` that `BALLPADDLE` reads to decide whether the paddle already caught the ball. Both suites green in both configurations, `docs_examples` and `docs_screenshots --check` pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
0679c3042b
|
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> |
|||
|
ad76889292
|
State where a scalar lives in the three chapters that describe the pool
Chapter 13's limits table said 4096 array elements "in total", Chapter 16 said an instance's fields come out of the same pool and nothing is reclaimed, and Chapter 14's pool table listed `AKBASIC_MAX_ARRAY_VALUES` with no note about what does and does not draw from it. All three were written when a scalar drew from that pool, and all three now understate what a program may do. Each gains the same two facts in the register it is written in. Chapter 13: a scalar does not come out of the 4096, so creating one inside a `GOSUB` or a `FOR` -- the loop counter included -- costs nothing, while a `DIM` inside a scope does and is not given back. Chapter 16: scalars are the exception, and "nothing is reclaimed" is what lets a pointer into a record stay sound after its scope has gone -- which is the reason arrays and structures still spend. Chapter 14: a row for the variable's own storage, and a paragraph on why the value pool is the one budget that needs a second sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
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> |
|||
|
c3d9e91bbd
|
Write down that a redraw has to fit inside one batch
"Drawing does not persist across frames; redraw it every frame" was the advice, and it is not sufficient on its own. The host runs a fixed number of source lines and then presents, and presenting discards the drawing buffer -- so a run of drawing verbs longer than one budget is **torn**, not merely transient, and an `SSHAPE` at the end of it captures only what was issued since the present, over whatever the frame before left behind. Measured against the standalone frontend's 256: after synchronising to a jiffy edge, 220 lines of drawing survive a capture and 250 do not. There is no fix available that does not change what a host owns -- the budget is the host's and so is the present -- so this is documentation, in the three places it belongs. Chapter 13 beside the note it qualifies, with the numbers. Chapter 6 where `SSHAPE` is introduced, which is where a program meets it. Chapter 14 from the step loop's side, naming `AKBASIC_FRONTEND_STEPS_PER_FRAME` and explaining the one thing a script can do about it: `settime()` is called once a frame, so `TI#` changes on the first step of a batch and nowhere else, and spinning until it changes is the only frame synchronisation this dialect has. **Untested, deliberately.** Reproducing it needs the real frontend, a clock and a timing window, and a test that reproduced it would be reproducing a race. Recorded in TODO.md rather than left looking like an oversight. TODO.md section 9 item 5, struck as a documentation outcome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
c8b917d205
|
Write down that the left operand decides integer or float arithmetic
No behaviour change, by decision. `A# * 0.45` is 0 and `0.45 * A#` is 1.35, because every operator branches on `self->valuetype` and converts the right operand to match. It is inherited from the Go reference, a C128 promotes to float instead, and this interpreter is at least consistent about it -- so a dialect saying the left operand wins is a defensible position, and changing it to promotion would alter the result of every mixed expression in every existing program. **The defect was that nobody said so.** Chapter 3's "Numbers" did not mention it, Chapter 13 did not list it among the differences, and nothing fails when a program gets it wrong -- it computes something else and carries on. The game in examples/ lost its per-level speed increase to `5.6 + LEVEL# * 0.45` evaluating to a flat 5.6, and bled velocity out of every bounce through `0 - BLVX%(B#)` quantising to whole pixels. Both read correctly. Neither produced a diagnostic. Now said in three places: a section in Chapter 3 with the demonstration and the two rules that keep a program out of it (put the float on the left, put the answer somewhere with a `%` on it), a row in Chapter 13 naming it as the difference from 7.0 most likely to turn a working listing into a quietly wrong one, and the reasoning on value.h where the operators are declared. tests/value_arithmetic.c pins it in both directions across multiply and subtract, with a comment saying it is the documented contract rather than an accident -- so promotion becomes a decision somebody takes deliberately rather than a change that could slip in under a passing suite. TODO.md section 9 item 4, struck as a documentation outcome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
a1dcfcacf3
|
Land a character written past a short row's terminator
A grid row is a NUL-terminated string, and `putchar_at()` wrote the character, advanced and terminated -- so `CHAR 1, 40, 1, "#"` on an otherwise empty row stored the `#` at column 40 with `text[1][0]` still `'\0'`, and the render loop, which stops at the terminator, drew nothing at all. The silent nothing is the trap. The write succeeded, the cursor moved, the stdout mirror showed the character, and only the window stayed blank -- so the program looked right everywhere except where it mattered. The documented truncation on the way *back* is fine and is unaffected. `putchar_at()` now fills the gap with spaces before storing. **Padded there rather than in `sink_moveto()`**, which TODO.md proposed: this way `moveto` stays read-only -- the objection that entry raised against its own suggestion -- and a row is only ever padded when a character actually arrives. The pad fills from the terminator rather than replacing it. The buffer is not cleared between rows, so replacing only the terminator would expose the tail of whatever longer row used to be there: writing "ABCDEFGHIJ", truncating it to "ABCX", then writing at column 7 must give "ABCX Z" and not "ABCX FGZ". tests/akgl_backends.c asserts all three cases, and the middle one keeps the truncation pinned. TODO.md section 6 item 32, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
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> |
|||
|
694b446ce4
|
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> |
|||
|
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> |
|||
|
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> |
|||
|
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> |
|||
|
8a50d07eef
|
Add the new refusal to the error-code appendix
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m11s
akbasic CI Build / sanitizers (push) Failing after 3m59s
akbasic CI Build / coverage (push) Failing after 3m26s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Failing after 3m16s
512 gains its new cause: a branch by number to a line the program did not number. PARSE ERROR is what the prescans report as, since they run before any statement does, so the class table says so. 514 claimed a branch to a line that does not exist raises it. Nothing does -- AKBASIC_ERR_UNDEFINED comes from label, variable, field and host-struct lookups only, and GOTO 500 with no line 500 walks to the end and stops. That was already wrong before this work; it is more visible now that a branch to a line that does exist can be refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
28ea99a638
|
Document optional line numbers
Chapter 2 gets the rule and the refusal, chapter 4 gets the payoff for LABEL, chapter 9 says DSAVE writes the numbers it handed out and to RENUMBER first if you want gaps, chapter 10 shows a host loading numberless source, chapter 13 gets the QuickBASIC-shaped divergence, and chapter 14's source[] passage gets its second half. Chapter 14 said "two prescans" and listed two; there were three before this and there are four now, so it lists all four and says which of them reports against the right line. TODO.md section 6 records four things found on the way and deliberately not fixed: set_label() filing into the active scope rather than the root, three prescans reporting the wrong line number, duplicate written line numbers still replacing silently, and renumber.c's file-scope scratch arrays. examples/embed.c runs the same program twice, numbered and not, so the example compiles the feature rather than describing it. Its header pointed at ./build/examples/embed, which is not where the binary lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
c1d06ee4b8
|
Let DEF take structure parameters, and give call scopes back
DEF AREA(S@ AS RECT) = S@.W# * S@.H#
DEF POKEIT(P@ AS PTR TO RECT)
A parameter names its type, exactly as DIM does. A bare `DEF F(S@)` is refused:
@ says "a structure" without saying which, so it does not state a contract the
way S$ does, and accepting it would mean checking fields at the call rather than
at the declaration -- which is the hole naming the type closes. The cost is that
there are no generic functions, and that is a real loss rather than an oversight.
Passing is by value, because a parameter is bound by assignment and assignment
copies; a pointer parameter copies its reference and lets a function change its
caller's record on purpose. Neither is a special rule. What a structure
parameter does need is its storage prepared before the copy, since a structure
variable is a run of slots and there is nothing to copy into until the run
exists.
A DEF parameter list is no longer parsed as an argument list, because a
parameter is a declaration rather than an expression: `S@ AS RECT` stopped that
parser dead with "Unbalanced parenthesis".
akbasic_value_is_truthy() learned that a pointer is true when it points at
something, which had to come with this. Without it there is no way to test for
the end of a list at all -- comparing a pointer to 0 reads a numeric field it
does not carry and answers whatever that field held. A structure is deliberately
given no truth value: it always exists, so the question has no answer worth
guessing at.
And a regression I introduced last commit, plus the older one underneath it.
prev_environment() released a scope but not the variables the scope created, so
a call leaked one slot per parameter and two hundred calls exhausted the
128-slot pool. Giving each DEF call its own scope made that reachable; it was
there for GOSUB all along, measured on a stashed build -- a subroutine with a
local of its own failed after about 128 calls before any of this work. The
release is safe because a scope's table holds only what it created, and it is
the variable slot that comes back rather than its storage, so a pointer into a
record DIMmed in that scope stays sound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
00daa17a47
|
Give each DEF call its own environment, so recursion returns
The function's environment was owned by the funcdef and re-initialised on every
call, which made a function not re-entrant and cost two silent defects:
DEF DBL(N#) = N# * 2
PRINT DBL(10) + DBL(1) was 4, should be 22
The result was a pointer into the funcdef's own environment, so the second call
overwrote the first before the operator saw it -- both operands became the last
call's answer. Two *different* functions in one expression were fine, which is
most of why it was invisible.
DEF FACT(N#)
IF N# <= 1 THEN RETURN 1
RETURN N# * FACT(N# - 1)
PRINT FACT(5) never returned
The recursive call re-initialised the environment the outer call was still
using, so the loop waiting for control to come back could not see it. No error,
no bound, no diagnostic -- the one place in this interpreter that looped forever
rather than raising.
A call takes an environment from the pool now, exactly as GOSUB does. The result
is copied into a caller-scope scratch before that environment goes back, because
handing back a pointer into the callee is what made two calls collide and would
now be a pointer into a released slot as well. RETURN parks its result on the
*parent* rather than on the environment it is about to release, so nothing reads
a freed slot to find it.
Recursion depth answers to AKBASIC_MAX_ENVIRONMENTS like every other nesting, so
too deep is "Environment pool exhausted" -- a diagnosis where there was none.
akbasic_FunctionDef.environment goes with it, as dead state.
One thing this exposed but did not cause, measured against a stashed build and
recorded rather than fixed: a statement containing a failed multi-line DEF call
still completes and prints a junk value. It is visible more often now only
because runaway recursion reaches it where it used to hang.
tests/language/functions/recursion.bas deliberately does not pin that answer.
Chapter 16 loses its "walk a list with a loop, not a recursive DEF" caveat and
gains the one that is still true: a function cannot take a structure parameter
yet, so it reaches a record by name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
6bac929901
|
Document structures: a chapter, the architecture, and the differences
docs/16-structures.md is the feature: records, nesting, copy-on-assign, strict pointers, lists, what is checked and what is not, and how a host shares its own C structs. Every example in it is executed by docs_examples and byte-compared, including the refusals -- so a message that changes fails the suite rather than quietly making the chapter wrong. The chapter makes one contrast explicitly, because it is the question a reader will actually have: a misspelled *field* is refused and a misspelled *variable* still prints zero. The rule underneath is that what the program declared gets checked and what it did not gets shrugged at -- a variable's name is never declared, a TYPE's field list is. Structures end up the strictest thing in the language, not from a higher standard but because they are the only named thing whose valid spellings are written down. Chapter 14 gains the layout: an instance is a contiguous run of value slots with a diagram of where the fields sit, the three-pass prescan and why each pass exists, why the copy cannot live in akbasic_value_clone(), and why the render depth bound is four rather than eight. Chapter 3 gains the @ suffix, chapter 13 records that all of this is an addition BASIC 7.0 has nothing like, and the verb reference gains TYPE, POINT and DIM ... AS. MAINTENANCE.md gains the two rules that are on a maintainer rather than on a test: a structure copy must not go through clone, and a field chain gets its own leaf field. TODO.md section 5 records what was invented and the three limits that are ours, and section 8 records the two defects the work exposed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
8077806598
|
Take libakerror 2.0.1, and guard the exit status it fixes
2.0.0 makes the error pool and the status registry thread safe, and it is an ABI break carrying the soname to libakerror.so.2. The break is a quiet one: __akerr_last_ignored became thread-local and akerr_next_error() now returns a context that already holds a reference, so objects compiled against a 1.x header and linked against 2.x count every reference twice and never give a slot back. Nothing about that fails to link, which is exactly what a guard is for -- include/akbasic/error.h feature-tests AKERR_THREAD_SAFE instead of AKERR_FIRST_CONSUMER_STATUS, which 2.0.0 also still defines and which therefore no longer distinguishes anything. 2.0.1 is the release this band needed most. The default unhandled-error handler ended in exit(errctx->status), and a process exit status is one byte: AKBASIC_ERR_BASE is 512, and 512 truncates to 0, so an unhandled AKBASIC_ERR_SYNTAX reported success to anything watching $?. Every other code in the band came out as some unrelated error's number. akerr_exit() substitutes 125 for anything a byte cannot carry, and a probe raising AKBASIC_ERR_DEVICE through FINISH_NORETURN now exits 125 rather than 7. It was latent here rather than live -- src/main.c handles the context and returns EXIT_FAILURE, and every test with a top-level ATTEMPT carries a HANDLE_DEFAULT -- but "no caller relies on it today" is not a property a header can keep true. tests/version_check.c asserts the mapping and fails if AKBASIC_ERR_BASE ever stops truncating to zero, because that is the day this stops being about our base. Chapter 10 gains a threading section: libakerror is safe from any thread now, and this interpreter is not and has no lock anywhere in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
16b38c1138
|
Generate the documentation's figures from the listings they illustrate
Chapters 6 and 8 described what a verb draws in prose. Eight figures now show it, and each one is produced by running the BASIC listing printed immediately above it -- so a picture cannot drift away from the code beside it, which is the way a screenshot goes wrong and the way nothing notices. tools/screenshot.c is a second SDL host, much smaller than the frontend: dummy video driver, software renderer, run to completion, read the target back, write a PNG. It draws no text layer on purpose, so a READY in the corner is not noise in a figure about BOX and no font has to be resolved. tools/docs_screenshots.sh reads the new screenshot=NAME fence tag straight out of the markdown. size=WxH is the second tag, and SCALE's figure uses it: the point being made is a 320x200 listing filling a larger window, which cannot be made on a 320x200 surface. Two gates, answering different questions. docs_examples fails a tagged block with no image, in both configurations, so a figure cannot be added and forgotten. docs_screenshots -- a CTest, AKGL build only -- re-renders every figure and compares byte for byte, so a listing edited without regenerating fails. Only the second catches a stale picture. The PNGs are checked in because a reader on the forge has no build tree, and docs/images/README.md says loudly that they are generated. Regenerating is never part of a build: the target is run deliberately, so a make cannot put eight binary diffs in front of whoever ran it. Drawing the BOX figure caught a defect in TODO.md itself. Deviation 16 claimed in bold that BOX fills on a negative angle while its own paragraph said the fill was filed rather than implemented. BOX cannot fill, and filled_rect is reached by no verb as a result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
737fdc760f
|
Document every error code a script can see, as chapter 15
ER# held numbers nothing explained. The appendix lists the four error classes the interpreter prints, the eight codes it owns and what raises each, and the codes that reach ER# from errno, libakerror and libakgl underneath it. The table is not asserted. A program in the chapter trips seven of the eight and prints what it got, and docs_examples byte-compares the result -- so the numbers are checked rather than claimed. The eighth, 516, is not usefully trappable and the chapter says why: entering a handler takes a scope, and the pool being empty is what raised it. Two things worth a reader's attention came out of writing it. Two codes register the same ERR() text, so a program must compare the number and print the text. And VAL reports libakerror's Value Error rather than the interpreter's 517, which makes that number the platform's rather than ours -- filed as section 6 item 21, not fixed here, because deciding which libakstdlib failures to translate is a boundary question and ENOENT out of DOPEN is the counter-case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
5c5bf63356
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an 800x600 window drew a C128 listing into its corner and left the rest unused -- while the chapter said coordinates were 320x200 and stretching to fit was the host's business. akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it before every verb that draws so a resized window is honoured between two statements, and 320x200 becomes the fallback for a backend that leaves it NULL. It is the record's one optional member, so a host written against the old header keeps the behaviour it had. SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's own field, the GRAPHIC mode. SCALE also mapped xmax onto the width rather than onto the last pixel, so SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel past the surface. Fixed in the same line, because it is what makes "SCALE gives a C128 listing the whole window" true rather than nearly true. The akgl test renders against a 128x128 target, deliberately smaller than the old constants: a SCALE still dividing by them misses it entirely rather than landing somewhere plausible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
23ccb66f69
|
Document the interpreter's architecture as chapter fourteen
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m50s
akbasic CI Build / coverage (push) Failing after 3m22s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Successful in 12m19s
Chapters 1 through 13 describe the language. Nothing described the machine that runs it, and the answers were spread across header comments, TODO.md sections written for a different purpose, and the source itself. Somebody embedding the interpreter, debugging something it did, or adding a verb had to reconstruct the shape from all three. docs/14-architecture.md is that shape, and only that: the three targets and the driver, the single akbasic_Runtime and why nothing is file-scope, akbasic_runtime_step() unrolled with the reason each stage sits where it does, the four modes and what set_mode() does beyond assigning, a line's journey from text through tokens and leaves to a verb handler, the dispatch table, the pool map with what each exhaustion actually says, environments doubling as block state, the two kinds of error, devices, and interrupts. It defers rather than restates. The headers are the authority on every function's contract and the chapter says so up front; where a rule is subtle the header comment already states it at more length than a chapter should. MAINTENANCE.md keeps the conventions and now points here for the mechanism, so there is still one copy of each. Two sections are the reason it exists at all. Debugging: reading a TRON trace as evidence about the loop rather than the lines, reading an akerror stack trace and what it is not, four breakpoints and the expressions worth printing at them, narrowing with ctest -R and the mock devices, and a symptom-to-cause table. Changing it: the verb recipe end to end including the private src/verbs.h prototype that is easy to miss, the rule that a missing dependency capability gets filed upstream rather than worked around, and the five constraints goal 3 puts on any change. A `text` fence tag comes with it. Every fenced block in docs/ is executed and an untagged one is a hard error, so six block diagrams had nowhere to live. The tag means never executed, it is counted in the skip line like `cmake`, and MAINTENANCE.md documents it -- the alternative was an indented block the extractor never sees, and a picture nobody decided about is indistinguishable from a test nobody ran. tests/docs_examples.sh now makes --root and --basic absolute before it starts. Both are used from inside a sandbox directory it cd's into, so the invocation MAINTENANCE.md itself documents -- --root . --basic ./build/basic -- failed every example with "exited 127" and every setup= with "setup failed". CTest passes absolute paths and never saw it; running one document by hand hits it immediately. Writing the error section turned up a defect and TODO.md section 8 records it. The ATTEMPT blocks that turn a script's mistake into an error line wrap parsing and interpretation but not scanning, so a line with more than 32 tokens escapes as an interpreter error: stack trace, exit 1, and at a prompt the REPL is gone. That is the same shape as section 8 item 2, on a path that fix did not cover. Not fixed here -- it is a behaviour change and wants its own tests -- but written down with the three call sites and what would cover them. Both configurations stay green: 95/95 and 94/94. docs_examples now runs 37 programs, 9 transcripts, 45 output comparisons, 3 C snippets, 2 excerpts and 2 shell blocks, and skips 9 text blocks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
80b76ad467
|
Split the documentation by who reads it
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m51s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled
README.md was 577 lines and answered four different questions at once: what
the project is, how to build it, every verb and function in the language, and
how to maintain the test harness. The verb and function lists had already been
written a second time in docs/11 and docs/12, which is how a list of that size
goes stale -- there is no way to notice the two have drifted apart.
README.md is now 150 lines and holds only what somebody evaluating the project
needs: what it is, the quickstart, why it was rewritten in C, the five rules
embedding imposes on the design, the two ways to use it, and where everything
else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md.
The akbasic_TextSink struct moved to docs/10-embedding.md rather than being
deleted. It was the corpus's only `c excerpt=` block -- the check that caught
the stale struct two commits ago -- so dropping it with the README would have
quietly retired a test. docs/10 also stopped claiming README.md carries the
full API surface and the pool limits, which the trim made false.
CLAUDE.md went from 458 lines to 62, because almost none of it was
agent-specific. The project goals, the Go reference and its architecture, the
dependency version and ABI rules, the four ways an embedded build collides,
the libakerror convention, the error-code range map and the style rules are
all things a maintainer needs, and they are now in MAINTENANCE.md with one
copy to keep true. CLAUDE.md points there and keeps only the rules no test
enforces: tests in the same commit asserting the correct contract, file a
missing dependency capability upstream, do not edit generated output or
tests/reference/, co-author your commits.
Four claims did not survive the move, having gone stale where nothing could
notice:
- "The repository is currently empty apart from its submodules -- no
commits, no source tree, no build files." There are 43 commits.
- libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE)
at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is
gone; only a historical mention in a comment remains.
- "akbasic_init() claims 512-767." There is no akbasic_init. It is
akbasic_error_register(), called from akbasic_runtime_init().
- Time-relative phrasing ("libakgl hit two of them in the last week").
Five places pointed at CLAUDE.md for the range map or the file-it-upstream
rule and now point at MAINTENANCE.md: include/akbasic/error.h,
src/runtime_disk.c and three entries in TODO.md. Both source changes are
comments. deps/libakgl/TODO.md cites it too and is left alone; it is a
submodule, and the rule it quotes is still reachable from CLAUDE.md.
ctest is green at 95 of 95, docs_examples included: 36 programs, 9
transcripts, 44 output comparisons, 3 C snippets, 1 excerpt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
342e4c07da
|
Execute every documented example as a test
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m52s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled
docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
cdaefcc941
|
Write the usage guide as thirteen chapters in docs/
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m0s
akbasic CI Build / sanitizers (push) Successful in 3m45s
akbasic CI Build / coverage (push) Failing after 3m22s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Successful in 11m26s
Organised the way the C128 Programmer's Reference Guide is: the language first, then each hardware area, then the reference sections. One markdown file per chapter. The verb and function references are generated from the interpreter's own dispatch table, with an assertion that every row is described, so they cannot drift out of step with what the program accepts. 98 verbs and 30 functions. Every example was run before it was written down, which caught three claims that were wrong: a whole FOR loop on one line prints nothing rather than looping once, MID and INSTR count from zero where a C128 counts from one, and a multi-line DEF returns a value the caller has to assign away. Chapter 13 is the list a BASIC 7.0 programmer needs -- roughly sixty documented differences, including the two known FOR defects and the fact that drawing does not survive a frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |