Commit Graph

12 Commits

Author SHA1 Message Date
f06c53110d Answer sprite collision through libakgl's narrowphase
`spr_collisions()` computed axis-aligned overlaps itself, because at libakgl
0.7.0 there was nothing to delegate to: `akgl_collide_rectangles()` has a
documented corner-containment defect and the physics backend's `collide` slot
raised "Not implemented". 0.8.0 brought a real narrowphase, and this moves onto
it.

The mask is bit-identical and every test from the previous commit passes
**unmodified**, which was the gate this stage had to clear -- including the two
that were written to be hard to satisfy. Edge-to-edge is still not a collision,
so a tile-aligned program is unaffected. The cross-shaped overlap is still
reported, which is the one that could have regressed: it is the case
`akgl_collide_rectangles()` gets wrong and the reason the hand-written loop
existed, and `akgl_collision_test()`'s box path gets it right.

What it buys is the contact -- a normal, a penetration depth and a point --
which four comparisons cannot produce. Nothing consumes it yet; that is the next
commit. It is here now because the mask and the contact come out of the same
test, and computing them in two places would be two things to keep in step.

**The first attempt was ten times slower and the benchmark caught it.** Syncing
all eight proxies and running the narrowphase on all twenty-eight pairs measured
984 ns a scan against 96 ns for the loop it replaced -- 21% of a frame at 256
scans a frame -- to produce a mask that was bit-identical and a contact that was
thrown away. Two changes fixed it, and both are what a broad phase *is* rather
than workarounds for a slow library:

- **Reject on the bounding boxes first.** The four comparisons that were always
  here now decide which pairs are worth an exact answer. The narrowphase still
  decides the bit -- the box test only says "maybe", which will matter the moment
  a shape is not the whole frame.
- **Do not sync a proxy that has not moved.** The scan runs at the top of every
  interpreter step and a sprite moves at most once in that time, so almost every
  sync would rewrite a proxy with what it already holds. Compared against the
  last synced rectangle rather than flagged by the verbs, because a host game can
  move a BASIC sprite through the actor registry and a flag would miss that.

Measured after: 54.9 ns at eight sprites spread out, which is *faster* than the
96.3 ns it replaced -- boxes are now built eight times a scan instead of
fifty-six. The number that matters is the new benchmark row for the arrangement
`examples/breakout/sprites/breakout.bas` actually has, which is 211.8 ns, or
4.5% of a frame. That game reaches it because two of its eight sprites are the
screen -- a captured HUD strip and a captured play field -- so the field's box
covers everything and those pairs can never be rejected. Roughly double the old
cost, for contacts. Recorded in MAINTENANCE.md with the two synthetic extremes
either side of it as a bracket.

The eight proxies are claimed once at init and held, so exhaustion of the pool
shared with an embedding host is an initialization failure that names the pool
rather than a collision scan refusing halfway through somebody's game. The shape
is built before the proxy is spawned from it and the acquire sits adjacent to the
initialize, which are two traps libakgl hit itself and documents.

`tests/akgl_backends.c` now tears the sprite backend down between cases. It never
did, and got away with it while init claimed nothing; eight proxies apiece across
twenty cases is a hundred and sixty against a pool of a hundred and twenty-eight.
A host releases what it took, and so does the harness.

Both games run forty seconds headless with no error line. 111 with akgl, 110
without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 09:48:11 -04:00
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
2026-08-02 09:35:22 -04:00
d61e352f36 Test the collision scan, and measure what it costs
`spr_collisions()` had no test. `tests/sprite_verbs.c` drives the collision path
end to end but through a mock backend, so the real overlap arithmetic in
`src/sprite_akgl.c` could have changed what `BUMP(1)` reports for every program
in existence and the suite would still have printed 110/110. That gap is closed
here, before anything touches the arithmetic, so that there is a *before* to
compare an *after* against.

Six cases against the real akgl backend: nothing defined, two sprites
overlapping, edge-to-edge, a hidden sprite, and the x-expand bit doubling the box
that collides rather than only the one that draws. Edge-to-edge earns its place
-- the test is a strict `<`, a tile-aligned program puts sprites there
constantly, and a replacement answering "touching" instead of "overlapping"
would change every one of them silently.

**The seventh is the cross-shaped overlap**, and it is the one to watch. A tall
thin sprite crossing a short wide one overlaps without either rectangle holding a
corner of the other; `akgl_collide_rectangles()` is documented as answering "no"
there, which is why `src/sprite_akgl.c` does the four comparisons itself rather
than calling it. Two further assertions stop that test passing by accident: each
sprite is moved clear along the axis it is supposed to be short on, so a sprite
that came out the wrong size fails rather than quietly reporting an ordinary
overlap.

`tests/collision_perf.c` answers the question nobody had measured. The service
runs at the top of every interpreter *step* and the frontend takes 256 steps per
rendered frame, so a busy program scans up to 256 times a frame over sprites that
have not moved. At RelWithDebInfo, scale 10, best of 5: the scan is 96.3 ns at
eight overlapping sprites and 19.4 ns at none, against a rendered frame of
1.17 ms. **256 scans is 24.7 us, or 2.1% of a frame, in the pathological case,
and 0.42% for a program with no sprites.**

So the per-step cadence stays. It is what makes a collision report describe where
the sprites have just been moved to rather than where they were, and 2% of a
frame in a case no real program reaches is not worth changing when a handler
fires for every program that already works. The numbers and that conclusion are
in `MAINTENANCE.md` so it does not get re-argued.

The benchmark borrows libakgl's `benchutil.h` by include path rather than copying
it, the way the fixture font is already borrowed, and is labelled `perf` so
`ctest -LE perf` can leave it out. It runs at scale 1 in the ordinary suite --
1.2 seconds -- because a benchmark nothing ever builds is a benchmark that rots.

Both suites green: 111 with akgl, 110 without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 09:25:22 -04:00
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
2026-08-02 08:08:23 -04:00
812d846e47 Take libakgl 0.7.0, and refuse an over-long type or field name
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m6s
akbasic CI Build / sanitizers (push) Failing after 3m59s
akbasic CI Build / coverage (push) Failing after 3m25s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Failing after 3m15s
0.6.0 and 0.7.0 break nothing here: 0.6.0 is three arcade-physics fixes and a
physics.max_timestep property this port does not use, and 0.7.0 reports failures
libakstdlib's wrappers were already catching and takes libakerror 2.0.1 so it
can drop the exit-status trap its own suites needed. That is the same defect
include/akbasic/error.h guards for this band, now fixed at the source rather
than worked around in two places.

The floor moves to 0.7.0 anyway. The soname carries MAJOR.MINOR while the major
is 0, so deciding for ourselves which of libakgl's minor releases were really
compatible is exactly the judgement it exists to take away.

0.7.0 is largely about a defect class worth checking for here rather than
assuming past: ten unterminated strncpy calls into fixed-width name fields. Ours
are clean -- every one is bounded to size - 1 and every buffer is either
explicitly terminated afterwards or memset first, checked site by site. But two
of them, both written last week in src/structtype.c, truncated an over-long name
silently, and truncation is an error everywhere else in this interpreter. It
matters more for a name than for a value: two long type names trimmed to the
same 31 characters collide, and the second is then reported as redeclaring a
type the program never wrote.

Both are refused now, showing the name as written. The check was unreachable as
first drafted, because the prescan read words into a buffer the size of the
limit and so trimmed them before anything could look -- the scratch is twice the
limit now, which is what makes the two cases distinguishable at all.
tests/struct_types.c pins that it stays reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:30:35 -04:00
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>
2026-08-01 12:03:02 -04:00
3415c9aec6 Take libakgl 0.5.0 and rename every symbol it namespaced
0.5.0 gives every exported symbol the akgl_ prefix, which is the first libakgl
release to break this project's source rather than only its ABI. The soname goes
to libakgl.so.0.5 and include/akbasic/akgl.h asserts the floor.

What moved here: akgl_render_bind2d is akgl_render_2d_bind,
akgl_sprite_sheet_coords_for_frame is akgl_spritesheet_coords_for_frame, the
renderer, camera and window globals carry the prefix, and _akgl_renderer and
_akgl_camera are akgl_default_renderer and akgl_default_camera.

The renames were applied by site rather than by pattern, because renderer is also
a parameter name in src/sprite_akgl.c and a struct member throughout
src/frontend_akgl.c -- a substitution would have rewritten both without a word.
That is the same trap upstream describes hitting, and it is worth knowing that
the defect behind the rename was not cosmetic: an exported global called renderer
collided with a test's own variable, the executable's definition preempted the
library's, and every texture load in that suite failed while the suite passed.

0.5.0 also fixes libakgl defect 26, which was one of the two reasons
src/sprite_akgl.c installs its own renderfunc: akgl_actor_render took its
destination height from the sprite's width, drawing a 24x21 Commodore sprite as a
24x24 square. The renderfunc stays, because the other reason has not moved -- an
actor carries one scalar scale and SPRITE has separate x- and y-expand bits --
but the comments and TODO.md deviation 40 no longer claim a defect that is fixed.

Every documentation figure re-renders byte-identical under the new library, which
is what says the sprite and drawing paths did not move underneath them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 08:01:43 -04:00
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>
2026-08-01 08:00:24 -04:00
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>
2026-08-01 07:37:04 -04:00
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>
2026-07-31 23:14:17 -04:00
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>
2026-07-31 22:59:43 -04:00
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>
2026-07-31 22:41:36 -04:00