Commit Graph

16 Commits

Author SHA1 Message Date
583c0abbd2 Retire the byte-for-byte fidelity constraint, and fix what it was blocking
The Go reference is deprecated and will not be updated, so the two projects
are no longer required to match. Recorded as TODO.md section 0.1, first thing
in the file, because it silently reverses the premise several later sections
were written on -- an agent reading section 6 without it will park work that
is no longer blocked.

Section 6 item 16 was the item waiting on exactly this and is now fixed: the
keyword tables are searched on the base name with any type suffix stripped, so
PRINT$, LEN# and GOTO% are refused as variable names and the reference's own
diagnostic stops being dead code. It cost the one golden case predicted, and
the cost was nothing -- renaming a variable in strreverse.bas left its expected
output byte-for-byte unchanged.

AKBASIC_KNOWN_FAILING_TESTS is empty as a result and known_reference_defects.c
is gone. Section 6 items 1-9 and 11 are reopened as an ordinary defect list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:52:00 -04:00
eb5374d212 Check the reference corpus in, so the build stops needing the Go submodule
All 41 .bas/.txt pairs copied byte for byte from basicinterpreter@d76162c into
tests/reference/, plus the Commodore font into assets/fonts/ -- the two things
that tied the build to deps/basicinterpret. Both were verified cmp-identical
to the submodule copies.

This reverses a decision that was deliberate and correct at the time: the
corpus was driven in place because copying a submodule's corpus guarantees
drift. Overruled on purpose -- the Go dependency is being deprecated, and a
build that cannot run its acceptance suite without cloning the implementation
it replaced is not finished. tests/reference/README.md records what the drift
now costs and that those expectations are never edited.

Checked rather than assumed: both configurations configure, build and pass
from scratch with deps/basicinterpret moved out of the tree entirely.

The submodule is kept as the behavioural spec, which is a real use. The font
came with an open licence question; assets/fonts/PROVENANCE.md states it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:35:40 -04:00
b5974121af Refresh the coverage numbers and document the new verbs
94.6% line, 98.6% function, measured from a coverage tree outside the source
directory. The akgl targets are not in that figure and the entry now says so,
because a reader of the table would otherwise read it as untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:09:02 -04:00
5b7b7d2ed9 Add akbasic_runtime_global: a host variable lands in the script's root scope
A host creating a variable while a script was suspended got it in whatever
scope was active -- usually a FOR or GOSUB body -- and it died when the body
popped, silently. Reaching for the root by hand returned NULL without raising,
because environment_get only auto-creates in the active environment.

Both are still true of environment_get, which is correct for what the
interpreter uses it for. The README and the example now point somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:53:05 -04:00
f24201fdef Port the standalone SDL frontend, and give the sink a line editor
An AKGL build of `basic` was a terminal program with unused SDL linked into
it. It now opens the reference's 800x600 window, draws BASIC output in the
Commodore font, pumps events, lets you type at it, and still mirrors every
byte to stdout.

The stdout mirror is a composing sink rather than a second write inside the
interpreter, and lives in the core library where it needs no SDL. The line
editor waits for a typed line by borrowing one frame at a time from the host,
so nothing blocks and nothing owns an event loop it should not.

The whole golden corpus now runs through the SDL binary as well as the stdio
one, byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:21:39 -04:00
ecdc6b60ef Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m52s
akbasic CI Build / sanitizers (push) Successful in 3m9s
akbasic CI Build / coverage (push) Failing after 3m12s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Successful in 7m27s
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.

Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.

The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.

tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.

Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.

One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.

72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
0df0a95600 Consume libakgl 0.2.0 and libakstdlib 0.2.0
Rebasing the defect filing onto libakgl's origin/main brought two upstream
commits with it -- "Consume libakstdlib 0.2.0" and "Bump libakgl to 0.2.0" --
and libakgl's sources now use the 0.2.0 akstdlib API. Our CMakeLists declares
akstdlib::akstdlib from our own tree first, so libakgl was compiling 0.2.0-era
code against 0.1.0 headers and failing on aksl_fwrite, aksl_fread and
aksl_realpath. The two have to move together.

The code change is one call site. aksl_fwrite now takes a required size_t
*nmemb_out and reports a short transfer as AKERR_IO rather than a silent
success, so DSAVE onto a full disk is an error a program sees where before it
reported nothing. The count is passed and discarded on purpose: the library does
the noticing now.

The documentation change is larger, because libakstdlib 0.2.0 fixed all six of
the confirmed defects TODO.md section 1.9 was built around. That section was a
table of bans; leaving it would send an agent around a workaround for functions
that now work. The aksl_ato* family raises AKERR_VALUE on no digits or trailing
junk and ERANGE on overflow -- exactly the contract that section demanded --
aksl_list_append no longer truncates, aksl_list_iterate no longer skips the
first half, AKERR_ITERATOR_BREAK stops a tree traversal, and aksl_realpath no
longer reads uninitialised memory. One caveat survives: aksl_strhash_djb2 still
sign-extends char, which section 1.3 already covers and the symbol tables still
cannot reach.

src/convert.c has therefore outlived its reason, and is deliberately left in
place. Its own note said to delete it when libakstdlib grew the contract, and
that condition is now met -- but doing it touches four call sites, changes the
raised status from AKBASIC_ERR_VALUE to AKERR_VALUE where section 1.8 says
message text is part of the acceptance contract, and would silently gut the CI
mutation job, which is bounded to src/convert.c and src/symtab.c. Section 1.9
now lists all of that. Worth doing on purpose rather than as a side effect of a
version bump.

libakgl defect #14 is closed by its own 1066ac7, which bumped it to 0.2.0 while
this was being written. The requirement is no longer pinned by submodule commit
in the README, because AKGL_VERSION_AT_LEAST(0, 2, 0) finally means something.

70/70 core, 71/71 with libakgl, 70/70 under ASan+UBSan, clean under
-Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:53:41 -04:00
ff7bec9f55 Record the real coverage numbers, and how they were nearly misreported
93.5% of lines and 97.8% of functions, up from 92.3% and 96.9% across roughly 800
new lines. The verb groups added for goal 2 carry the new surface: the two table
files and runtime_input.c at 100%, runtime_graphics.c and runtime_audio.c at 98%,
play.c at 87%.

The first reading of that number was wrong and the reason is worth keeping. A
stale build-cov/ left in the source directory by an earlier session was silently
folded in by `gcovr --root .`, which reported the *previous* run's 92.3% for a
tree that had grown by 800 lines -- a number that looked plausible precisely
because it had not moved. That is libakgl defect #13 happening here rather than
there, and build*/ being gitignored is what makes it invisible. Section 8 now
says to check `find . -name '*.gcda'` before believing a coverage figure that
looks suspiciously unchanged.

Also documents the akgl build and test commands in the README, which had none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:51:27 -04:00
4fc763efb3 Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.

The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.

tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.

Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:

- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
  be *installed*. It builds its own vendored copies only when it is top-level,
  and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
  with if(NOT TARGET ...), so this adds those five subdirectories before
  add_subdirectory(deps/libakgl) -- the same trick and the same ordering
  requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
  akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
  akgl_render_init2d() installs the vtable but also creates its own window and
  writes the camera global, so it belongs to the akgl_game_init() path -- which
  is exactly the path an embedding host is not on. The test assigns the six
  pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
  reaches through renderer->draw_texture without checking it. Same class of
  defect 42b60f7's own commit added a draw test for.

The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.

70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
5077efc3a7 Re-sequence the language queue now that libakgl closed the gaps
The bump in 7f16337 updated sections 3, 7 and 8 but left the section 4 work
queue still marking groups G and I "file it", which is no longer true.

Groups G and I are unblocked outright. Group E splits: GET, GETKEY and SCNCLR
need nothing further, while WINDOW, KEY, SLEEP, WAIT and TI still want the sink
or a clock. LOCATE was listed in both E and G; it belongs to G alone, because in
BASIC 7.0 it moves the graphics pixel cursor DRAW starts from and the text
cursor verb is CHAR, already in group D.

One gap is genuinely outstanding and upstream named it in the same commit that
added the audio API: FILTER has no SDL3 primitive behind it. It is recorded in
section 7 with the behaviour to apply until an akgl_audio_filter() exists --
refuse at execution rather than silently ignore, since a program that asks for a
low-pass and gets an unfiltered square wave has been lied to.

Also records why the libakgl requirement is pinned by commit rather than by
version: 42b60f7 added 22 public symbols across four headers and left both
VERSION 0.1.0 and the libakgl.so.0.1 soname alone, so AKGL_VERSION_AT_LEAST
cannot tell the two trees apart and a binary built against the new headers
resolves against an old .so without complaint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:01:16 -04:00
7f16337a16 Bump libakgl to 42b60f7: every gap this port filed is now closed
All four capability gaps filed against libakgl have landed upstream, so nothing
in this repository is waiting on that library any more.

  text measurement   -> akgl_text_measure, akgl_text_measure_wrapped
  immediate drawing  -> akgl_draw_point/_line/_rect/_filled_rect/_circle/
                        _flood_fill/_copy_region/_paste_region
  audio              -> akgl_audio_init/_tone/_envelope/_waveform/_volume/
                        _stop/_voice_active/_mix
  console input      -> akgl_controller_poll_key, akgl_controller_flush_keys

The signatures came back close to what was filed, and two details are worth
recording for whoever writes src/sink_akgl.c next. The draw calls take an
akgl_RenderBackend * as their first argument rather than reaching for a global
renderer, which fits goal 3's rule that the interpreter draws through whatever
renderer the host already initialized. And the audio API is a synthesised-voice
one -- a tone on a numbered voice plus a separate ADSR envelope -- which is the
shape PLAY and ENVELOPE actually need, rather than the sample playback
SDL3_mixer would have offered.

Checked rather than assumed: libakgl's status band is still 256-260, so the
coordinated range map in CLAUDE.md is unaffected and akbasic's 512-767 does not
move.

Documentation that described these as open is corrected in the same commit --
section 7's gap list, section 3's "known gap" note, the priority list, the
dependency table, and the README's unimplemented-verbs section all said blocked
and no longer are. Section 3 also gains the thing that replaced the gap: the
character grid comes from akgl_text_measure(font, "A", &w, &h), the direct
equivalent of the font.SizeUTF8("A") at basicruntime.go:96, and
akgl_text_measure_wrapped takes the same wraplength argument
akgl_text_rendertextat does so the measurement and the draw cannot disagree
about where a line breaks.

One caveat recorded rather than papered over: -DAKBASIC_WITH_AKGL=ON has never
been configured in this repository, because until now there was nothing to build
against. The akbasic_akgl target is unproven and needs libakgl's own submodules
present. Expect to fix something there on the first attempt.

Default build unaffected: ctest 61/61, doxygen exits 0, no warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:56:02 -04:00
506af093a1 Add mutation testing, and split the release gates into their own workflow
Ports libakstdlib's mutation harness (the newest of the three) to scripts/
mutation_test.py, adds the namespaced `mutation` CMake target the sibling
libraries have, and splits CI in two.

.gitea/workflows/ci.yaml keeps the push path: suite, ASan+UBSan, coverage, and a
mutation run bounded to src/convert.c and src/symtab.c -- about four minutes,
scoring 77.8% against a gate of 65.

.gitea/workflows/release.yaml is new and manual (workflow_dispatch). It carries
the doxygen gate, moved out of ci.yaml, and a whole-tree mutation run: 3675
mutants and hours of runner time, which is a release cost rather than a
per-commit one. Both artifacts a release wants -- api-documentation and
mutation-report -- come out of it. Two optional inputs narrow the run or change
the threshold; they arrive through the environment rather than being
interpolated into the shell, because ${{ }} substitution happens before the
shell sees the line.

The harness paid for itself immediately, which is the point of it. Three real
gaps, each checked to be a genuine bug rather than an equivalent mutant:

  - errno was never asserted clear before a strtoll. Confirmed with a standalone
    probe that strtoll leaves a stale errno untouched on success, so without the
    `errno = 0` a valid conversion raises ERANGE.
  - Nothing exercised a maximum-length symbol-table key, so every MAX_KEY - 1
    off-by-one in a strncpy and its NUL terminator survived. The same hole exists
    for strings in src/value.c and is filed.
  - Nothing asserted a freshly initialised table was actually zeroed.

Closing the first two took the measured score from 73.1% to 77.8%.

I set the push-path threshold to 75 first, on an estimate. Measuring gave 73.1%
and the job would have failed on its first run -- the earlier per-file figure was
too high because the captured output had been truncated to its last lines and I
counted fewer survivors than there were. It is 65 now, and the gate was run as
written and confirmed to exit 0.

src/value.c is the file most worth mutating and is deliberately off the push
path: 368 mutants at ~11s each is about 70 minutes, because almost everything
links against it. A partial run over it found the same maximum-length-string
hole plus two genuinely equivalent mutants that only exist because of reference
defect section 6 item 5 -- adding both of the right operand's numeric fields
works only while the unused one is zero, so + and - are interchangeable there.
Recorded in TODO.md.

ctest 61/61; doxygen exits 0; both workflows' steps were executed locally, both
input paths included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:46:53 -04:00
2ddee571a1 Add a Gitea workflow
Fills the gap TODO.md recorded last commit: the sibling libraries all carry
.gitea/workflows/ci.yaml and this one did not, so every gate was run by hand.
Four jobs, in the shape libakerror and libakstdlib use, down to the JUnit
reporting and the annotate_only note about Gitea 404ing on the Checks API.

cmake_build configures, builds and runs the 61-case suite with JUnit output.
docs runs `doxygen Doxyfile` -- a real gate, since WARN_AS_ERROR is set -- and
uploads build/docs/html as the api-documentation artifact. sanitizers runs the
whole suite under ASan and UBSan, which libakstdlib's TODO.md calls its own
highest-value missing item and which matters here because this library is all
fixed pools and manual buffer arithmetic. coverage gates at 90% of lines against
an actual 92.3% and uploads the html report.

Two things worth knowing, both checked rather than assumed.

The checkout is submodules: true, not recursive. The build needs libakerror,
libakstdlib and basicinterpret; it does not need libakgl, which is guarded
behind AKBASIC_WITH_AKGL and defaults OFF, and recursing would clone SDL,
SDL_image, SDL_mixer, SDL_ttf and jansson for a target that is never configured.
The nested submodules are uninitialized in this working tree today and the build
is green, which is the proof. The docs job takes no submodules at all -- verified
by running doxygen against a tree with deps/ absent.

There is no branch-coverage gate. Branch coverage reads 17.3% and is not a
meaningful number here, for the reason libakgl's and libakstdlib's TODO.md both
record: the akerror control-flow macros expand into large branch trees at every
call site, most unreachable in normal operation. Gating on it would mean writing
tests for libakerror's macros.

All four jobs were executed locally end to end before committing, not just
eyeballed: 61/61 plain, 61/61 under ASan+UBSan, doxygen exit 0 producing 441
files, and gcovr exit 0 at the 90 gate. The gate was also confirmed to bite by
raising it to 99.

Not added: a mutation-testing job, which all three siblings have. There is no
scripts/mutation_test.py in this repository to run, and porting libakerror's is
its own piece of work -- filed in TODO.md, where it matters more than usual
because the akerror macros expand at their call sites and coverage cannot see
them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:09:38 -04:00
134ade9392 Document the public API in libakgl's Doxygen style
Adds a Doxyfile in the shape libakgl uses -- fifteen deliberate lines, including
WARN_IF_UNDOCUMENTED and WARN_AS_ERROR=FAIL_ON_WARNINGS -- and fills in the 70
public declarations that had no doc block, bringing include/akbasic to 114 of
114.

libakgl is the model rather than libakstdlib. Measured before starting: libakgl
documents 122 of 122 public declarations and libakstdlib 3 of 25, and
libakstdlib's Doxyfile is the unedited doxygen default, PROJECT_NAME = "My
Project" and an empty INPUT. So the house standard is libakgl's, down to the
boilerplate phrasing for the recurring parameters -- "Object to initialize,
inspect, or modify", "Output destination populated by the function", "`NULL` on
success, otherwise an error context owned by the caller".

Worth knowing what the gate actually gates. EXTRACT_ALL=YES suppresses
doxygen's undocumented-entity warnings, so the rule it enforces is that a
*partial* block is an error: document one @param and you must document them all.
Verified by deleting a @param and confirming a non-zero exit, then restoring it.
Full coverage is therefore a convention this commit adopts rather than something
the tool made me do.

Where a contract is non-obvious the block says so rather than restating the
signature: math_plus explains why it alone mutates its left operand, new_unary
notes that hanging the operand on .right is what makes the parser miscount a
negative literal argument, leaf_to_string warns that an assignment renders with
an empty operator, and stop_waiting records that a verb nobody is waiting for is
tolerated. Each cross-references its TODO.md section 6 item.

Also records in TODO.md that this repository has no CI, which libakgl and
libakstdlib both have -- so ctest, the sanitizer build, coverage and this new
doxygen gate are all run by hand today.

ctest 61/61; doxygen Doxyfile exits 0; no warnings under -Wall -Wextra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:00:16 -04:00
e51b9a1694 Document host/script variable exchange, and file the scope hazard it exposes
The host can expose variables to a BASIC program, in both directions and for
every type, with no marshalling layer -- akbasic_environment_get() finds or
creates by name, the type comes from the suffix as it does for BASIC code, and
host and script share the same akbasic_Value. Nothing new was needed to make
that work.

Testing it before writing it up turned up a hazard worth stating plainly, so the
README documents the pattern and section 6 item 17 records the gap.

Seeding before akbasic_runtime_start() and reading after the script stops is
safe, and so is updating an existing global at any time, because the parent
chain is searched. Creating one mid-run is not, in two ways, and both are
silent. A script suspended part-way through a bounded run() is usually inside a
FOR or GOSUB scope, so a variable created through obj->environment lands in that
scope and dies when it pops -- the script reads it correctly inside the loop and
gets 0 immediately after. And reaching for the root explicitly does not help:
akbasic_environment_get only auto-creates when the environment it is given is
the active one, so with a child active it returns NULL through dest without
raising, and an unchecked host dereferences it.

This one is ours rather than inherited. It falls out of the environment pool
meeting the bounded run(), a combination the reference never had because its
run() never returned.

examples/hostvars.c demonstrates both hazards rather than describing them, and
prints what it observes, so the day this is fixed that output changes and the
example needs revisiting. Like examples/embed.c it is built and run by every
build. Both README snippets were extracted and compiled as a check.

Not fixed here: akbasic_runtime_global() would close it in about fifteen lines,
but what it should do when the script is suspended inside a user function's
scope is a design question worth settling deliberately rather than discovering.
Filed with the proposed signature and the tests it wants.

ctest 61/61; ASan+UBSan 61/61; no warnings under -Wall -Wextra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:50:55 -04:00
cde6fa8f59 Port the README from the Go version and document embedding
Carries the reference README across, adjusted where C changes the answer: cmake
instead of make, the ak* libraries instead of the go-sdl2 bindings, and the
limits table now includes the three ceilings the Go version did not need because
it called make().

The new "Embedding the interpreter" section is the point of the rewrite, so it
states the four rules the library holds to -- nothing terminates the process,
nothing calls malloc, no file-scope mutable state, the host owns the loop -- and
each was checked against the tree rather than asserted.

Adds akbasic_runtime_load(). Writing the section turned up a real gap: a host
usually already holds its script as a string and wants the sink reserved for
output, and the only path that existed was AKBASIC_MODE_RUNSTREAM reading the
program through the sink's readline, which forces a game to point its output
device at its source text. The alternative was reaching into the header's
"internal API" block for store_line. Neither is something to put in a README.

Adds examples/embed.c, which is the code the README quotes -- a custom sink, a
bounded per-frame run, and the PASS-not-CATCH rule for a loop inside an ATTEMPT.
It is built by every build and registered as a CTest case, so a signature change
breaks the build instead of rotting the document. The README's own snippet is
compiled separately as a check; both were run before committing.

The "What Isn't Implemented / Isn't Working" section leads with the eleven
inherited defects rather than burying them, because five of them were found by
this port and a reader deserves to know that 1 - 2 - 3 computes 1 - 2 before
they hit it. Corrected two claims while verifying: the runtime is 10.1MB rather
than the ~8MB first written, and its largest single cost is the environment pool
at 4.1MB, not the source table.

ctest 60/60; ASan+UBSan 60/60; no warnings under -Wall -Wextra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:42:08 -04:00