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>
28 KiB
This BASIC is styled after Commodore BASIC 7.0 and the Dartmouth BASIC from 1964. It is a C rewrite of basicinterpreter, which was itself built from the instructions for the Java implementation of Lox in craftinginterpreters.com before striking off on its own. The Go version is vendored at deps/basicinterpret and is the behavioural specification: when a question about semantics comes up, the answer lives in that code and in its tests/.
git submodule update --init --recursive
cmake -S . -B build
cmake --build build --parallel
# To use the interactive REPL
./build/basic
# To run a basic file from the command line
./build/basic deps/basicinterpret/tests/language/functions.bas
# The test suite: unit tests plus the Go version's own corpus, byte-compared
ctest --test-dir build --output-on-failure
# API documentation, into build/docs/html
doxygen Doxyfile
# With the libakgl-backed sink and devices. Off by default, because this pulls in
# SDL3 and the whole interpreter builds and tests without it.
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
cmake --build build-akgl --parallel
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy ctest --test-dir build-akgl --output-on-failure
There are two workflows. .gitea/workflows/ci.yaml runs on every push: the suite,
ASan+UBSan, coverage gated at 90% of lines, and mutation testing over two files. The coverage
report is uploaded as a code-coverage artifact.
.gitea/workflows/release.yaml is manual (workflow_dispatch) and is what a release runs.
It builds the API documentation and uploads it as api-documentation, and it mutates the
whole src/ tree — 3675 mutants, hours of runner time, which is why it is not on the push
path. It takes two optional inputs: a mutation threshold, and a space-separated file list to
narrow the run.
Mutation testing is worth a word, because it is the only gate that checks the error-handling
control flow at all. libakerror's ATTEMPT/CATCH/PASS macros expand at their call sites,
so gcov attributes them to the caller and line coverage cannot see them. scripts/mutation_test.py
breaks the library many small ways and checks that the suite notices:
cmake --build build --target mutation # the whole src/ tree; slow, hours
python3 scripts/mutation_test.py --target src/value.c --list
python3 scripts/mutation_test.py --target src/value.c --threshold 70
It earns its keep. Writing this suite, it found that nothing exercised a maximum-length string
or symbol-table key, so every MAX - 1 off-by-one in a strncpy would have gone unnoticed;
and that errno was never asserted to be cleared before a strtoll, which is what stops a
stale ERANGE from failing a perfectly valid conversion.
The Doxyfile is configured the way libakgl's is, including
WARN_AS_ERROR = FAIL_ON_WARNINGS — a doc block that documents some of a function's
parameters but not all of them fails the run, so doxygen Doxyfile is a gate rather than a
convenience. Every one of the 114 public declarations under include/akbasic/ carries a
@brief, a @param per parameter, a @return and its @throws.
Why rewrite it in C?
Three reasons, in the order they matter.
The interpreter is meant to end up inside libakgl as a scripting engine for game authors, and libakgl is C. Embedding a Go runtime in a C game is not a thing anybody should do to themselves.
The Go version was already written against static pools and explicit state structs — [MAX_SOURCE_LINES]BasicSourceLine, a fixed variable pool, a 32-leaf ceiling per line — so it ports across almost directly. It reads like C that happens to be spelled in Go. Rewriting it in the idiom of libakerror and libakstdlib was less work than it sounds.
And the port is a good excuse to find out what the original actually does, as opposed to what it looks like it does. It found five defects nobody knew about. See "What Isn't Implemented / Isn't Working", below.
What Works?
Everything the Go version does. All 41 .bas files in deps/basicinterpret/tests/ produce byte-identical output from both implementations, including error messages and the trailing blank line after one. Those files are driven in place as individual CTest cases rather than copied, so the corpus cannot drift from upstream.
Case Sensitivity
The old computers BASIC was originally written on only had CAPITAL LETTER KEYS on their keyboards. Modern keyboards have the indescribable luxury of upper and lower case. In this basic, verbs and function names are case insensitive. Variable names are case sensitive.
Variables
A#Integer variablesA%Float variablesA$String variables. Strings support addition operations with other types.LETis supported but optional- Variables are strongly typed
Note that % means float here, which inverts the Commodore convention where % is integer. That is what the Go version does and it is not being changed out from under anybody.
Arrays
DIM IDENTIFIER(DIMENSION[, ...])allows for provisioning of multiple dimensional arraysDIM A$(3)results in a single dimensional array of strings with 3 elementsPRINT A$(2)accesses the last element in an array and returns it to the verbLEN(A#)on an array returns its total element count- Arrays are strongly typed
Expressions
+-^*(also works on strings)/< <= <> == >= >less than, less than equal, not equal, equal, greater equal, greater than
Expressions can be grouped with () arbitrarily deeply. Currently the interpreter has a limit of 32 tokens and leaves per line. In effect this means about 16 operations in a single line.
Commands (Verbs)
The following commands/verbs are implemented:
AUTO n: Turn automatic line numbering on/off at increments ofnREM: everything after this is a commentDATA LITERAL[, ...]: Define a series of literal values that can be read by a precedingREADverbDEF FN(X, ...) = expression: Define a function with arguments that performs a given expression. See also "Subroutines", below.DELETE [n-n]: Delete some portion of the lines in the current programDELETE: Delete ALL lines in the programDELETE n-n: Delete lines betweennandn(inclusive)DELETE -n: Delete lines from 0 tonDELETE n: Delete lines fromnto the end of the program
DLOAD FILENAME: Load the BASIC program in the file FILENAME (string literal or string variable) into memoryDSAVE FILENAME: Save the current BASIC program in memory to the file specified by FILENAME (string literal or string variable)EXIT: Exit a loop before it would normally finishFOR: Iterate over a range of values and perform (statement) or block each time.
10 FOR I# = 1 TO 5
20 REM Do some stuff in here
30 NEXT I#
10 FOR I# = 1 TO 5 STEP 2
20 REM Do some stuff here
30 NEXT I#
GOTO n: Go to line n in the programGOSUB n: Go to line n in the program and return here whenRETURNis foundIF (comparison) THEN (statement) [ELSE (statement)]: Conditional branchingINPUT "PROMPT STRING" VARIABLE: Read input from the user and store it in the named variableLABEL IDENTIFIER: Place a label at the current line number. Labels are constant integer identifiers that can be used in expressions like variables (including GOTO) but which cannot be assigned to. Labels do not have a type suffix ($,#or%).LIST [n-n]: List all or a portion of the lines in the current program, with the same range forms asDELETEPOKE ADDRESS, VALUE: Poke the single byte VALUE into the ADDRESSPRINT (expression)QUIT: Exit the interpreterREAD IDENTIFIER[, ...]: Fill the named variables with data from a subsequent DATA statementRETURN: return fromGOSUBto the point where it was calledRUN [n]: Run the program currently in memory, optionally starting at linenSTOP: Stop program execution at the current point
Functions
The following functions are implemented
ABS(x#|x%): Return the absolute value of the float or integer argumentATN(x#|x%): Return the arctangent of the float or integer argument. Input and output are in radians.CHR(x#): Return the character value of the UTF-8 unicode codepoint in x#. Returns as a string.COS(x#|x%): Return the cosine of the float or integer argument. Input and output are in radians.HEX(x#): Return the string representation of the integer number in x#INSTR(X$, Y$): Return the index ofY$withinX$(-1 if not present)LEN(var$): Return the length of the objectvar$(either a string or an array)LEFT(X$, Y#): Return the leftmost Y# characters of the string in X$. Y# is clamped to LEN(X$).LOG(X#|X%): Return the natural logarithm of X#|X%MID(var$, start, length): Return a substring fromvar$MOD(x#, y#): Return the modulus of ( x / y). Only works on integers.PEEK(X): Return the value of the BYTE at the memory location of integer X and return it as an integerPOINTER(X): Return the address in memory for the value of the variable identified in X. This is the direct integer, float or string value stored, it is not a reference to the internal variable structure.POINTERVAR(X): Return the address in memory of the variable X. This is the address of the internalakbasic_Variablestructure, which includes additional metadata about the variable, in addition to the value. For a pointer directly to the value, usePOINTER.RAD(X#|X%): Convert degrees to radiansRIGHT(X$, Y#): Return the rightmost Y# characters of the string in X$. Y# is clamped to LEN(X$).SGN(X#): Returns the sign of X# (-1 for negative, 1 for positive, 0 if 0).SHL(X#, Y#): Returns the value of X# shifted left Y# bitsSHR(X#, Y#): Returns the value of X# shifted right Y# bitsSIN(X#|X%): Returns the sine of the float or integer argument. Input and output are radians.SPC(X#): Returns a string of X# spaces. This is included for compatibility, you can also use(" " * X)to multiply strings.STR(X#): Returns the string representation of X.TAN(X#|X%): Returns the tangent of the float or integer variable X. Input and output are in radians.VAL(X$): Returns the float value of the number in X$XOR(X#, Y#): Performs a bitwise exclusive OR on the two integer arguments
Unlike the Go version, none of these are bootstrapped by running a BASIC program of DEF statements through the interpreter at startup. A builtin's name, arity and handler are one row in the dispatch table in src/verbs.c, which means the interpreter no longer has to be running before the interpreter is ready.
Subroutines
In addition to DEF, GOTO and GOSUB, this BASIC also implements subroutines that accept arguments, return a value, and can be called as functions. Example
10 DEF ADDTWO(A#, B#)
20 C# = A# + B#
30 RETURN C#
40 D# = ADDTWO(3, 5)
50 PRINT D#
Subroutines must be defined before they are called. Subroutines share the global variable scope with the rest of the program.
Embedding the interpreter
The whole point of the rewrite. libakbasic is a static library with a thin src/main.c driver on top; the REPL, argv handling and QUIT belong to the driver, not to the library. A host program links the library and keeps control.
Four rules the library holds to, because a game engine cannot tolerate a scripting language that breaks any of them:
- Nothing terminates the process. No
exit(), noabort(), nopanic. Errors come back asakerr_ErrorContext *.FINISH_NORETURNnever appears in the library at all — only in amain(), which today means the driver's and the embedding example's. - Nothing calls
malloc. Every object comes from a fixed pool insideakbasic_Runtime. Exhausting one is a diagnosable error, not a crash and not a slow leak. - No file-scope mutable state. Interpreter state lives in an
akbasic_Runtimeyou own. Two of them in one process do not interfere. - The host owns the loop.
akbasic_runtime_run(rt, n)executes at mostnsource lines and returns. A script with an infinite loop costs younlines per frame and nothing else.
A complete, compiled, runnable example is in examples/embed.c — it is built by every build and registered as a test, so it cannot rot. The shape is:
#include <akerror.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
/*
* The runtime carries every pool the interpreter owns -- a few megabytes -- so
* it goes in static storage or inside your own game state, never on the stack.
*/
static akbasic_Runtime SCRIPT;
static const char *PROGRAM =
"10 PRINT \"COUNTING:\"\n"
"20 FOR I# = 1 TO 5\n"
"30 PRINT I# * I#\n"
"40 NEXT I#\n";
akerr_ErrorContext AKERR_NOIGNORE *script_start(akbasic_TextSink *sink)
{
PREPARE_ERROR(errctx);
PASS(errctx, akbasic_runtime_init(&SCRIPT, sink));
PASS(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
SUCCEED_RETURN(errctx);
}
/* Call this once per frame. Eight source lines, then back to your renderer. */
akerr_ErrorContext AKERR_NOIGNORE *script_tick(void)
{
PREPARE_ERROR(errctx);
if ( SCRIPT.mode == AKBASIC_MODE_QUIT ) {
SUCCEED_RETURN(errctx);
}
PASS(errctx, akbasic_runtime_run(&SCRIPT, 8));
SUCCEED_RETURN(errctx);
}
Exchanging variables with the host
Yes — in both directions, for integers, floats and strings, using the same variable pool the
script itself uses. There is no marshalling layer and no copy: the host and the script are
looking at the same akbasic_Value.
akbasic_environment_get() finds a variable by name and creates it if it does not exist. The
type comes from the name's suffix, exactly as it does for BASIC code, so HP# is an integer
and NAME$ is a string. Every scalar is really a one-element array, which is why the subscript
list is {0} with a count of 1.
/* host -> script, before the script starts */
akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
int64_t subscript[1] = { 0 };
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY,
"could not reach variable %s from the active scope", name);
PASS(errctx, akbasic_variable_set_integer(variable, value, subscript, 1));
SUCCEED_RETURN(errctx);
}
/* script -> host, after it stops */
akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char *name, int64_t *dest)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
akbasic_Value *value = NULL;
int64_t subscript[1] = { 0 };
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY, "no variable %s", name);
PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &value));
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"%s is not an integer", name);
*dest = value->intval;
SUCCEED_RETURN(errctx);
}
Used like this, with akbasic_variable_set_string and set_float as the other two:
CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); /* seed */
CATCH(errctx, host_set_int(&SCRIPT, "LEVEL#", 7));
CATCH(errctx, host_set_string(&SCRIPT, "NAME$", "LINK"));
CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
CATCH(errctx, akbasic_runtime_run(&SCRIPT, 0));
CATCH(errctx, host_get_int(&SCRIPT, "SCORE#", &score)); /* collect */
The full thing, including the string and float forms, is in
examples/hostvars.c. It is built and run by every build.
The one rule: seed before, read after
Do this between runs, not during one. Seed before akbasic_runtime_start() and read after
the script has stopped. Poking a variable while a script is suspended part-way through a
bounded akbasic_runtime_run() is sharp in two ways, and both are silent:
- A suspended script is usually inside a
FORorGOSUBscope, andobj->environmentis that scope rather than the global one. A variable created there dies when the scope pops — the script reads it correctly inside the loop and gets0the moment the loop ends. - Reaching for the global scope explicitly does not help. Only the currently active environment
auto-creates, so
akbasic_environment_get(root, "NEW#", &var)with a child active hands backNULLand no error, and an unchecked host dereferences it.
Reading and updating an existing global is safe at any time — the parent chain is searched, so a variable that already exists in the global scope is found from anywhere. It is only creation that lands in the wrong place.
examples/hostvars.c demonstrates both hazards rather than describing them. This is filed as
TODO.md section 6 item 17; the fix is a small akbasic_runtime_global() that
resolves against the root scope regardless of what is active.
Where the output goes
PRINT writes through an akbasic_TextSink, which is a record of function pointers plus whatever state you hang off self:
typedef struct akbasic_TextSink
{
void *self;
akerr_ErrorContext AKERR_NOIGNORE *(*write)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
} akbasic_TextSink;
akbasic_sink_init_stdio() ships with the library and is what the driver and the golden-file suite use. A game supplies its own and draws into a text layer. The interpreter never owns a window, a renderer or an event loop, and readline is expected to set *eof rather than block — that is how INPUT behaves sanely inside a frame.
Error codes
The library reserves status values 512–767 with libakerror's registry, under the owner string "akbasic". akbasic_runtime_init() claims the range for you and is idempotent, so calling it twice is harmless and calling it after your own initialization is fine. If something else in the process already owns part of that band, init fails loudly rather than silently aliasing your error codes onto somebody else's.
For reference, the coordinated map across this dependency stack is libakerror 0–255, libakgl 256–260, and akbasic 512–767.
Linking
add_subdirectory(deps/akbasic EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
akbasic links akerror::akerror and akstdlib::akstdlib publicly, so you inherit both. If your project already declares those targets, declare them before adding this one — the same rule that applies to libakgl.
libakgl itself is not a dependency of the core library. -DAKBASIC_WITH_AKGL=ON builds an additional akbasic_akgl target carrying the akgl-backed text sink and the graphics, sound and input backends; it is off by default, which is why the interpreter and its whole test suite build on a machine with no SDL.
The graphics, sound and console verbs reach hardware through records of function pointers on the runtime — akbasic_GraphicsBackend, akbasic_AudioBackend and akbasic_InputBackend, attached with akbasic_runtime_set_devices(). All three may be NULL, which is what the standalone driver gives them: a verb that needs a device it was not given raises rather than crashing, and a PRINT-only program never notices. A host that renders some other way supplies its own records and never links libakgl at all.
Two things about those verbs that differ from a real C128, because both would otherwise surprise you. PLAY does not block — it queues its notes and akbasic_runtime_step() releases them against whatever time you last passed to akbasic_runtime_settime(), so the statement after a PLAY runs immediately. And GETKEY holds the program without blocking you: the step still returns, it simply does not advance until a key arrives.
Limits
Everything is bounded and pre-declared. The numbers are in include/akbasic/types.h and are the Go version's, plus three the Go version did not need because it called make():
| Constant | Value | What it bounds |
|---|---|---|
AKBASIC_MAX_LEAVES / _TOKENS |
32 | AST nodes and tokens per source line (~16 operations) |
AKBASIC_MAX_VALUES |
64 | Intermediate values per line |
AKBASIC_MAX_VARIABLES |
128 | Variables per scope |
AKBASIC_MAX_SOURCE_LINES |
9999 | Program length |
AKBASIC_MAX_LINE_LENGTH |
256 | Characters per line |
AKBASIC_MAX_STRING_LENGTH |
256 | Characters in a string value |
AKBASIC_MAX_ENVIRONMENTS |
32 | Nesting depth of FOR and GOSUB |
AKBASIC_MAX_ARRAY_ELEMENTS |
1024 | Elements in one array |
AKBASIC_MAX_ARRAY_VALUES |
4096 | Array elements across all variables |
Raising any of them costs BSS and nothing else. An akbasic_Runtime is presently 10.1MB, and it is worth knowing where that goes before you reach for a knob: 4.1MB is the environment pool, 2.5MB the 9999-line source table, 2.3MB the function-definition pool and 1.2MB the array value pool. AKBASIC_MAX_ENVIRONMENTS is the expensive one — each environment carries its own token, leaf and value arrays — so halving it to 16 saves twice what halving the source table does.
What Isn't Implemented / Isn't Working
Defects inherited from the Go version
These are reproduced deliberately, not fixed. The Go version's test corpus is the acceptance suite, so a silent correction here is a behaviour change that would show up as a failing golden file. Each is catalogued in TODO.md section 6 with the file and line, and tests/known_reference_defects.c asserts the correct contract for six of them under AKBASIC_KNOWN_FAILING_TESTS — when one is fixed, CTest reports "unexpectedly passed", which is the cue to move it.
Five of them the port found; nobody knew about these before:
1 - 2 - 3computes1 - 2. Subtraction stops after one operator where addition loops, so the rest of the line is abandoned in the token stream. This is the bad one — it is a wrong answer, not a refused one.2 ^ 3 ^ 2has the same shape.- A negative literal cannot be passed to a builtin.
ABS(-9)is rejected with "function ABS takes 1 arguments, received 2", because the arity counter walks the same.rightpointer a unary-minus leaf keeps its operand on. Assign to a variable first, as the Go version's ownsgn.bastest quietly does. - A comparison operator in a line's final column is dropped.
A# =produces one token, not two, because the scanner cannot peek past the end of the line and gives up without recording the operator. - Hex literals do not work.
0xfflexes as0xfollowed by an identifierff. The base-16 branch in the literal parser is unreachable, so the hex support the Go README implies has never existed. PRINT$is accepted as a variable name. The "Reserved word in variable name" check compares the lexeme including its type suffix against the keyword table, so it never matches and never fires.
Plus six more the original already carried: a leading 0 selects base 8, so PRINT 010 prints 8 and PRINT 08 will not parse; setBoolean tags the value it builds TYPE_STRING; stopWaiting ignores its argument, so an inner block can clear an outer block's pending wait; toString on a variable has its emptiness test inverted; EXIT pops a loop without clearing that wait; and mathPlus mutates its left operand in place where every other operator clones. That last one is load-bearing rather than merely wrong — NEXT relies on it to advance the loop counter, so it cannot be fixed on its own.
Not implemented
- Multiple statements on one line (e.g.
10 PRINT A$ : REM This prints the thing). TheCOLONtoken exists and nothing consumes it. - Using an array reference inside a parameter list (e.g.
READ A$(0), B#) results in parsing errors APPEND,BACKUP,BEGIN,BEND,BLOAD,BOOT,BOX,BSAVECATALOG,CHAR,CIRCLE,CLOSE,CLR,CMD,COLLECT,COLLISION,COLOR,CONCAT,CONT,COPYDCLEAR,DCLOSE,DIRECTORY,DOPEN,DRAW,DVERIFYDO,LOOP,WHILE,UNTIL. You can do the same thing withIFandGOTO.END,ENVELOPE,ER,ERRFETCH,FILTERGET,GETKEY,GRAPHIC,GSHAPEHEADER,HELPKEY,LOAD,LOCATEMOVSPR,NEW,ON,PAINT,PLAY,PUDEFRENAME,RENUMBER,RESTORE,RESUMESAVE,SCALE,SCNCLR,SCRATCH,SLEEP,SOUNDSPRCOLOR,SPRDEF,SPRITE,SPRSAV,SSHAPE,STASH,SWAP,SYSTEMPO,TI,TRAP,TROFF,TRONUSING,VERIFY,VOL,WAIT,WIDTH,WINDOW- The I/O-channel variants (
GETIO,INPUTIO,OPENIO,PRINTIO,RECORDIO)
One of those is still blocked on a missing libakgl capability, and it is only one. Four were — text measurement, immediate-mode drawing, audio, and a non-blocking keystroke read — and rather than work around them here they were filed in deps/libakgl/TODO.md under "API gaps blocking akbasic". All four have since landed upstream (akgl_text_measure, the akgl_draw_* family, akgl_audio_*, and akgl_controller_poll_key), so what is left is akbasic-side work.
The exception is FILTER, which sets the SID's filter cutoff, band switches and resonance. akgl_audio_* synthesises raw waveforms and mixes them; there is no filter stage to configure, and SDL3 supplies no primitive to build one from. Until an akgl_audio_filter() exists, FILTER parses and then refuses at execution rather than being silently ignored — a program that asks for a low-pass and gets an unfiltered square wave has been lied to.
Deliberately out of scope
BANK- the modern PC memory layout is incompatible with the idea of bank switchingFAST- Irrelevant on modern PC CPUsMONITOR- there is no machine-language monitor to drop into
Dependencies
- libakerror 1.0.0 — TRY/CATCH-style error contexts. Every function that can fail returns one.
- libakstdlib 0.1.0 — libc wrappers that report through
libakerror. - libakgl 0.1.0 at commit
42b60f7or later — optional, only for-DAKBASIC_WITH_AKGL=ON. Pulls in SDL3. The requirement is pinned by submodule commit rather than by version on purpose: 42b60f7 added 22 public symbols and left the version and thelibakgl.so.0.1soname alone, soAKGL_VERSION_AT_LEAST(0, 1, 0)cannot tell the two trees apart. Filed upstream;TODO.mdsection 8 has the detail. - basicinterpret — the Go original, vendored as the behavioural spec and the acceptance corpus. Not linked, not built.
Everything is a submodule; git submodule update --init --recursive gets all of it. There is nothing to install first.
Note that libakstdlib's aksl_atoi/atol/atoll/atof family is deliberately not used here, because it cannot report a conversion failure — atoi("not a number") returns success with 0. src/convert.c wraps strtoll/strtod with the strict contract instead, and TODO.md section 1.9 records which of that library's calls are cleared for use. That file will be deleted when libakstdlib grows the wrappers its own TODO.md section 3.1 already calls for.