Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
/**
|
|
|
|
|
* @file parser_commands.c
|
|
|
|
|
* @brief Verbs that need their own parse path rather than a plain expression.
|
|
|
|
|
*
|
|
|
|
|
* Ported from basicparser_commands.go. Two of these mutate runtime state from
|
|
|
|
|
* inside the parser and that is not an accident: DEF installs the function so a
|
|
|
|
|
* later line can call it, and FOR builds and installs the loop's environment
|
|
|
|
|
* before the body is ever scanned. The waitingForCommand scheme depends on the
|
|
|
|
|
* latter.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
#include <string.h>
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
#include <strings.h>
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/parser.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
|
|
|
|
|
#include "verbs.h"
|
|
|
|
|
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The generic "this verb takes a comma-separated list" path, shared by the
|
|
|
|
|
* graphics and sound verbs. The default command path parses a *single*
|
|
|
|
|
* expression, which quietly turns COLOR 0, 1 into COLOR 0 with a stray 1 --
|
|
|
|
|
* so every verb taking more than one argument needs this rather than NULL.
|
|
|
|
|
*
|
|
|
|
|
* The verb name comes back out of the token stream rather than being passed
|
|
|
|
|
* in, because a parse handler is dispatched by name and the table has no
|
|
|
|
|
* column to carry one.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
/*
|
|
|
|
|
* Checked before parsing rather than after. Handing an empty token stream to
|
|
|
|
|
* the expression parser fails deep inside it with "peek() returned nil
|
|
|
|
|
* token!", which tells a program author nothing at all -- and this path is
|
|
|
|
|
* reached by every verb that takes a list, so the bad message was the one
|
|
|
|
|
* most people saw.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (akbasic_parser_peek(parser) != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"%s expected at least one argument", operator_->lexeme);
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"%s expected at least one argument", operator_->lexeme);
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs
|
|
|
|
|
* instead of a comma. TO is a reserved word the argument list will not cross,
|
|
|
|
|
* so the pairs after the first are appended by hand and the whole thing
|
|
|
|
|
* flattens to one argument chain: source, x1, y1, x2, y2, ... The exec
|
|
|
|
|
* handler walks it in pairs and never has to know a TO was involved.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"DRAW expected a color source and a coordinate");
|
|
|
|
|
|
|
|
|
|
tail = arglist->right;
|
2026-07-31 11:58:15 -04:00
|
|
|
while ( tail->next != NULL ) {
|
|
|
|
|
tail = tail->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for ( ;; ) {
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked == NULL ||
|
|
|
|
|
peeked->tokentype != AKBASIC_TOK_COMMAND ||
|
|
|
|
|
strcmp(peeked->lexeme, "TO") != 0 ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "DRAW expected TO");
|
2026-07-31 11:58:15 -04:00
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
"DRAW expected X after TO");
|
2026-07-31 11:58:15 -04:00
|
|
|
tail = tail->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "DRAW expected TO X,Y");
|
2026-07-31 11:58:15 -04:00
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
"DRAW expected Y after TO X,");
|
2026-07-31 11:58:15 -04:00
|
|
|
tail = tail->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
akerr_ErrorContext *akbasic_parse_movspr(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *form = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
const char *formlexeme = "0";
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* MOVSPR is the one verb in the language whose arguments are not simply
|
|
|
|
|
* comma-separated. It has four forms and the separators are what tell them
|
|
|
|
|
* apart:
|
|
|
|
|
*
|
|
|
|
|
* MOVSPR n, x, y absolute
|
|
|
|
|
* MOVSPR n, +x, -y relative -- a signed coordinate, not a negative one
|
|
|
|
|
* MOVSPR n, d ; a polar: distance, then angle
|
|
|
|
|
* MOVSPR n, a # s continuous: angle, then speed
|
|
|
|
|
*
|
|
|
|
|
* So the form has to be decided here, where the tokens still exist, and
|
|
|
|
|
* carried to the exec handler somehow. It is carried as a synthetic integer
|
|
|
|
|
* literal prepended to the argument list, which flattens the whole statement
|
|
|
|
|
* to `form, n, a, b` -- the same trick akbasic_parse_draw uses to make a
|
|
|
|
|
* polyline look like a plain argument chain.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &form));
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"MOVSPR expected a sprite number");
|
|
|
|
|
tail = arglist->right;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "MOVSPR expected a comma after the sprite number");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A leading sign makes it relative. `+` has to be consumed here because the
|
|
|
|
|
* expression parser has no unary plus; `-` is left where it is, because
|
|
|
|
|
* unary minus is a real operator and re-parsing it would drop the sign.
|
|
|
|
|
*/
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked != NULL &&
|
|
|
|
|
(peeked->tokentype == AKBASIC_TOK_PLUS || peeked->tokentype == AKBASIC_TOK_MINUS) ) {
|
|
|
|
|
formlexeme = "1";
|
|
|
|
|
if ( peeked->tokentype == AKBASIC_TOK_PLUS ) {
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"MOVSPR expected a coordinate, a distance or an angle");
|
|
|
|
|
tail = tail->next;
|
|
|
|
|
|
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON) ) {
|
|
|
|
|
formlexeme = "2";
|
|
|
|
|
} else if ( akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK) ) {
|
|
|
|
|
formlexeme = "3";
|
|
|
|
|
} else {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"MOVSPR expected `, y', `; angle' or `# speed'");
|
|
|
|
|
if ( formlexeme[0] == '0' ) {
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_PLUS ) {
|
|
|
|
|
formlexeme = "1";
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
|
|
|
} else if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_MINUS ) {
|
|
|
|
|
formlexeme = "1";
|
|
|
|
|
}
|
|
|
|
|
} else if ( akbasic_parser_peek(parser) != NULL &&
|
|
|
|
|
akbasic_parser_peek(parser)->tokentype == AKBASIC_TOK_PLUS ) {
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"MOVSPR expected a second value");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_literal_int(form, formlexeme));
|
|
|
|
|
form->next = arglist->right;
|
|
|
|
|
arglist->right = form;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Read an optional `WHILE`/`UNTIL` condition off a DO or a LOOP.
|
|
|
|
|
*
|
|
|
|
|
* Both ends of a DO/LOOP take the same optional clause, so both parse it the
|
|
|
|
|
* same way. @p kind comes back as one of the AKBASIC_LOOPCOND_* values and
|
|
|
|
|
* @p condition is NULL when there was no clause.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_ASTLeaf **condition, int *kind)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
|
|
|
|
|
*condition = NULL;
|
|
|
|
|
*kind = AKBASIC_LOOPCOND_NONE;
|
|
|
|
|
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
if ( strcmp(peeked->lexeme, "WHILE") == 0 ) {
|
|
|
|
|
*kind = AKBASIC_LOOPCOND_WHILE;
|
|
|
|
|
} else if ( strcmp(peeked->lexeme, "UNTIL") == 0 ) {
|
|
|
|
|
*kind = AKBASIC_LOOPCOND_UNTIL;
|
|
|
|
|
} else {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
|
|
|
|
|
|
|
|
/* A loop condition is a condition, so a lone `=` in it is an equality test. */
|
|
|
|
|
parser->comparing = true;
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, condition));
|
|
|
|
|
parser->comparing = false;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (*condition != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected a condition after WHILE or UNTIL");
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* DO [WHILE|UNTIL (condition)]
|
|
|
|
|
*
|
|
|
|
|
* Pushes the loop's scope at parse time, exactly as FOR does and for the same
|
|
|
|
|
* reason: the condition leaf has to outlive the line it was written on, and an
|
|
|
|
|
* environment's leaf pool is what gives it somewhere to live.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
|
|
|
akbasic_Environment *parent = runtime->environment;
|
|
|
|
|
akbasic_Environment *newenv = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *condition = NULL;
|
|
|
|
|
int kind = AKBASIC_LOOPCOND_NONE;
|
|
|
|
|
int64_t firstline = parent->lineno + 1;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
|
|
|
|
newenv = runtime->environment;
|
|
|
|
|
runtime->environment = parent;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Parsed against the parent's token stream but stored in the new scope --
|
|
|
|
|
* the body is scanned line by line afterwards, and the new scope must not be
|
|
|
|
|
* active until parsing is done.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, parse_loop_condition(parser, &condition, &kind));
|
|
|
|
|
|
|
|
|
|
newenv->isDoLoop = true;
|
|
|
|
|
newenv->loopFirstLine = firstline;
|
|
|
|
|
newenv->doConditionKind = kind;
|
|
|
|
|
if ( condition != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_leaf_clone(condition, &newenv->doLeafPool, &newenv->doConditionLeaf));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "DO", NULL));
|
|
|
|
|
|
|
|
|
|
runtime->environment = newenv;
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* LOOP [WHILE|UNTIL (condition)]
|
|
|
|
|
*
|
|
|
|
|
* The condition hangs off the command leaf's `.right`, with the WHILE/UNTIL
|
|
|
|
|
* choice in that leaf's integer literal and the expression on its `.left`. No
|
|
|
|
|
* cloning: this line is still scanned when the verb runs.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_loop(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *holder = NULL;
|
|
|
|
|
akbasic_ASTLeaf *condition = NULL;
|
|
|
|
|
int kind = AKBASIC_LOOPCOND_NONE;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, parse_loop_condition(parser, &condition, &kind));
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
if ( condition != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &holder));
|
|
|
|
|
PASS(errctx, akbasic_leaf_init(holder, AKBASIC_LEAF_LITERAL_INT));
|
|
|
|
|
holder->literal_int = kind;
|
|
|
|
|
holder->left = condition;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "LOOP", holder));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* ON (expression) GOTO|GOSUB (target) [, ...]
|
|
|
|
|
*
|
|
|
|
|
* Flattened to one argument chain: a marker carrying the GOSUB flag, the
|
|
|
|
|
* selector expression, then the targets. Same trick akbasic_parse_draw and
|
|
|
|
|
* akbasic_parse_movspr use, so the exec handler walks a plain list.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *marker = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
bool gosub = false;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &marker));
|
|
|
|
|
PASS(errctx, akbasic_leaf_init(marker, AKBASIC_LEAF_LITERAL_INT));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &marker->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (marker->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected ON (expression) GOTO|GOSUB (line) [, ...]");
|
|
|
|
|
tail = marker->next;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
if ( strcmp(operator_->lexeme, "GOSUB") == 0 ) {
|
|
|
|
|
gosub = true;
|
|
|
|
|
} else {
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "GOTO"), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected GOTO or GOSUB after ON (expression)");
|
|
|
|
|
}
|
|
|
|
|
marker->literal_int = (gosub ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
for ( ;; ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected a line number or label after ON ... %s",
|
|
|
|
|
(gosub ? "GOSUB" : "GOTO"));
|
|
|
|
|
tail = tail->next;
|
|
|
|
|
if ( !akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
arglist->right = marker;
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "ON", arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* RESUME [NEXT | (line)]
|
|
|
|
|
*
|
|
|
|
|
* `NEXT` is a COMMAND token, so the default command path -- which parses the
|
|
|
|
|
* rval as an expression -- cannot read it: `RESUME NEXT` came back as "Expected
|
|
|
|
|
* expression or literal". The word is kept as the command leaf's rval so the
|
|
|
|
|
* exec handler can tell the three forms apart by looking at it.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_resume(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *target = NULL;
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND &&
|
|
|
|
|
strcmp(peeked->lexeme, "NEXT") == 0 ) {
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &target));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL));
|
|
|
|
|
} else if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &target));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "RESUME", target));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* PRINT [USING (format);] (expression)
|
|
|
|
|
*
|
|
|
|
|
* Only the USING form needs a parse handler; without it PRINT keeps the default
|
|
|
|
|
* path, which parses one expression. `USING` is a COMMAND token, so the default
|
|
|
|
|
* path could not read it -- the same reason RESUME needed one for `NEXT`.
|
|
|
|
|
*
|
|
|
|
|
* The result is a command leaf whose rval is an argument list of exactly two:
|
|
|
|
|
* the format and the value. A plain PRINT keeps its single-expression rval, so
|
|
|
|
|
* the exec handler tells them apart by looking for the list.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *right = NULL;
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* `PRINT #1, expr` writes to a file channel. A C128 spells it `PRINT#1` with
|
|
|
|
|
* no space, which this scanner cannot produce: `PRINT#` reads as an
|
|
|
|
|
* identifier with an integer suffix, and a verb name carrying a suffix is
|
|
|
|
|
* refused (deviation 30). So the `#` is its own token and the space before
|
|
|
|
|
* it is required. Recorded in TODO.md section 5.
|
|
|
|
|
*
|
|
|
|
|
* The channel form is marked by a leaf named "PRINT#" holding an argument
|
|
|
|
|
* list of two: the channel and the value.
|
|
|
|
|
*/
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) {
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK);
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PRINT #(channel), (expression)");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected a comma after PRINT #(channel)");
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PRINT #(channel), (expression)");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT#", arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ||
|
|
|
|
|
strcmp(peeked->lexeme, "USING") != 0 ) {
|
|
|
|
|
/* The ordinary PRINT: one expression, or none at all. */
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &right));
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", right));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PRINT USING (format); (expression)");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected a semicolon between PRINT USING's format and its value");
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PRINT USING (format); (expression)");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The argument list, for verbs that may be written with no arguments at all --
|
|
|
|
|
* bare `KEY` lists the macros, bare `RENUMBER` renumbers with its defaults.
|
|
|
|
|
* `akbasic_parse_arglist` insists on at least one argument, which is right for
|
|
|
|
|
* every other verb that uses it.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_optional_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
|
|
|
PASS(errctx, akbasic_parse_arglist(parser, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, NULL));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
akerr_ErrorContext *akbasic_parse_let(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* LET is optional in this dialect and in Commodore BASIC 7.0. Assignment is
|
|
|
|
|
* handled by expression evaluation, so LET parses as a bare assignment and
|
|
|
|
|
* its exec handler does nothing.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_parser_assignment(parser, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* LABEL and DIM share a shape: the verb, then one identifier. */
|
|
|
|
|
static akerr_ErrorContext *parse_verb_with_identifier(akbasic_Parser *parser, const char *verbname, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected identifier");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, verbname, identifier));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_label(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "LABEL", dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
/**
|
|
|
|
|
* @brief `DIM NAME(SIZES)`, or `DIM NAME@ AS TYPE` / `DIM NAME@ AS PTR TO TYPE`.
|
|
|
|
|
*
|
|
|
|
|
* The `AS` clause is read here and parked on the *command* leaf: `literal_string`
|
|
|
|
|
* takes the type name and `literal_int` records whether `PTR TO` was written.
|
|
|
|
|
* Both fields are free on a command leaf, which is the same reasoning the field
|
|
|
|
|
* leaf uses for `.left` -- this file's header note about three defects from one
|
|
|
|
|
* overloaded link field is about giving a field two meanings on the *same* leaf
|
|
|
|
|
* type, not about using an unused one.
|
|
|
|
|
*
|
|
|
|
|
* `AS`, `PTR` and `TO` are matched by lexeme rather than promoted to keywords.
|
|
|
|
|
* Making them reserved words would break any existing program with a variable
|
|
|
|
|
* called `AS#`, and they are only meaningful in this one position.
|
|
|
|
|
*/
|
Add strict pointers: AS PTR TO, POINT ... AT, and the -> operator
A pointer is a distinct declared kind rather than a mode a structure can be in,
which is what "strict" means here: `.` requires a structure on its left and `->`
requires a pointer, neither stands in for the other, and both refusals name the
operator the program should have used. So a reader always knows from the
spelling whether the thing on the left is their own copy or somebody else's
data.
Assignment still copies. POINT is the only way to share, so a program that never
writes it can never be surprised by aliasing -- and `P@ = A@` is refused with a
message saying to POINT it instead, rather than quietly becoming the one
assignment in the language that does not copy.
PTR TO is also the only way a TYPE may refer to itself, since by value it would
have no finite size. That is what makes a linked list possible, and
tests/language/structures/pointers.bas builds one, walks it and renders it.
Rendering follows pointers, so it needs a depth bound where copying does not:
copy stops at a pointer by construction, but two nodes pointing at each other is
easy to write and PRINT would not come back. Four levels, chosen so the bound
bites before the 256-byte render buffer does -- otherwise a cycle would stop
because it ran out of room rather than because it was told to.
Three things the work turned up, all now pinned by tests:
A freshly DIMmed record printed `(UNDEFINED STRING REPRESENTATION FOR 0)` for
every field. Slots now take the type their field declared, so it reads as zeros.
Adding POINT as a verb makes POINT unusable as a type name, and the parser
reported that as "Expected expression or literal" pointing at the line rather
than the problem. The prescan now refuses a reserved word as a type name and
says so.
Field names follow the same reserved-word rule as variable names, enforced by
the loader's own scan -- `TO@` is a bad field name for exactly the reason `TO#`
is a bad variable name. Recorded rather than worked around.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:48:09 -04:00
|
|
|
/**
|
|
|
|
|
* @brief `POINT P@ AT target`.
|
|
|
|
|
*
|
|
|
|
|
* Two expressions with a word between them rather than a comma, because that is
|
|
|
|
|
* how it reads: the verb says what is being done and `AT` says which way round
|
|
|
|
|
* the two operands go. `POINT P@, A@` would read equally well in either
|
|
|
|
|
* direction, and getting it backwards would silently point the wrong thing.
|
|
|
|
|
*
|
|
|
|
|
* The two are chained through `.next`, the argument link, so the runtime reaches
|
|
|
|
|
* them with akbasic_leaf_first_argument() like any other verb's operands.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_point(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *pointer = NULL;
|
|
|
|
|
akbasic_ASTLeaf *target = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
akbasic_Token *word = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &pointer));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected POINT <pointer> AT <structure>");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AT") == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected AT after POINT <pointer>, not %s", word->lexeme);
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &target));
|
|
|
|
|
|
|
|
|
|
pointer->next = target;
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
arglist->right = pointer;
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "POINT", arglist));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
/**
|
|
|
|
|
* @brief `TYPE NAME`, whose body was already read by the prescan.
|
|
|
|
|
*
|
|
|
|
|
* Only the opening line is ever parsed: the verb jumps past its own `END TYPE`,
|
|
|
|
|
* so the field lines never reach the parser at all. That is deliberate -- a
|
|
|
|
|
* field line is a declaration, and `W#` on its own would otherwise evaluate as a
|
|
|
|
|
* bare identifier and quietly create a global called `W#`.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_type(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "TYPE", dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
akbasic_Token *word = NULL;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
|
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest));
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
command = *dest;
|
|
|
|
|
|
|
|
|
|
if ( !akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER) ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
if ( strcasecmp(word->lexeme, "PTR") == 0 ) {
|
|
|
|
|
command->literal_int = 1;
|
Add strict pointers: AS PTR TO, POINT ... AT, and the -> operator
A pointer is a distinct declared kind rather than a mode a structure can be in,
which is what "strict" means here: `.` requires a structure on its left and `->`
requires a pointer, neither stands in for the other, and both refusals name the
operator the program should have used. So a reader always knows from the
spelling whether the thing on the left is their own copy or somebody else's
data.
Assignment still copies. POINT is the only way to share, so a program that never
writes it can never be surprised by aliasing -- and `P@ = A@` is refused with a
message saying to POINT it instead, rather than quietly becoming the one
assignment in the language that does not copy.
PTR TO is also the only way a TYPE may refer to itself, since by value it would
have no finite size. That is what makes a linked list possible, and
tests/language/structures/pointers.bas builds one, walks it and renders it.
Rendering follows pointers, so it needs a depth bound where copying does not:
copy stops at a pointer by construction, but two nodes pointing at each other is
easy to write and PRINT would not come back. Four levels, chosen so the bound
bites before the 256-byte render buffer does -- otherwise a cycle would stop
because it ran out of room rather than because it was told to.
Three things the work turned up, all now pinned by tests:
A freshly DIMmed record printed `(UNDEFINED STRING REPRESENTATION FOR 0)` for
every field. Slots now take the type their field declared, so it reads as zeros.
Adding POINT as a verb makes POINT unusable as a type name, and the parser
reported that as "Expected expression or literal" pointing at the line rather
than the problem. The prescan now refuses a reserved word as a type name and
says so.
Field names follow the same reserved-word rule as variable names, enforced by
the loader's own scan -- `TO@` is a bad field name for exactly the reason `TO#`
is a bad variable name. Recorded rather than worked around.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:48:09 -04:00
|
|
|
/*
|
|
|
|
|
* `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a
|
|
|
|
|
* command token rather than an identifier. Matched by lexeme here for
|
|
|
|
|
* the same reason AS and PTR are: these three words mean something only
|
|
|
|
|
* in this one position, and reserving them would break any program with
|
|
|
|
|
* a variable called TO#.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
Add records: TYPE, DIM ... AS, field access, copy on assign
BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.
Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.
Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.
The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.
Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.
Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:39:47 -04:00
|
|
|
AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
}
|
|
|
|
|
snprintf(command->literal_string, sizeof(command->literal_string), "%s", word->lexeme);
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* DEF NAME (A, ...) [= expression]
|
|
|
|
|
* COMMAND IDENTIFIER ARGUMENTLIST [ASSIGNMENT EXPRESSION]
|
|
|
|
|
*
|
|
|
|
|
* With an `=` the function is a single expression. Without one it is a
|
|
|
|
|
* multi-line subroutine whose body starts on the next line and ends at RETURN,
|
|
|
|
|
* so the environment is told to skip forward to that RETURN rather than execute
|
|
|
|
|
* the body during the definition.
|
|
|
|
|
*/
|
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>
2026-08-01 13:05:32 -04:00
|
|
|
/**
|
|
|
|
|
* @brief A `DEF` parameter list, which is not an ordinary argument list.
|
|
|
|
|
*
|
|
|
|
|
* `akbasic_parser_argument_list()` parses *expressions*, and a parameter is a
|
|
|
|
|
* declaration -- `S@ AS RECT` is not an expression and stops that parser dead
|
|
|
|
|
* with "Unbalanced parenthesis". So this reads the list itself.
|
|
|
|
|
*
|
|
|
|
|
* **A structure parameter must name its type.** `@` says "a structure" without
|
|
|
|
|
* saying which: three primitive types fit in three suffix characters and N
|
|
|
|
|
* declared types do not fit in one, so `X$` fully states its contract and `X@`
|
|
|
|
|
* does not. A bare `X@` is refused rather than treated as "any structure",
|
|
|
|
|
* because that is duck typing and it puts back exactly the hole naming the type
|
|
|
|
|
* closes. The cost is that there are no generic functions, and it is a real one.
|
|
|
|
|
*
|
|
|
|
|
* The type name is parked on the parameter leaf in `literal_string`, with
|
|
|
|
|
* `literal_int` recording `PTR TO` -- the same two free fields, for the same
|
|
|
|
|
* reason, that `DIM ... AS` uses.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *param = NULL;
|
|
|
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
|
|
|
akbasic_Token *word = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF <name>");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, ¶m));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(param != NULL &&
|
|
|
|
|
(param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
|
|
|
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
|
|
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT ||
|
|
|
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT)),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Only variable identifiers are valid arguments for DEF");
|
|
|
|
|
|
|
|
|
|
if ( param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT ) {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME",
|
|
|
|
|
param->identifier, param->identifier, param->identifier);
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected AS after %s, not %s", param->identifier, word->lexeme);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
if ( strcasecmp(word->lexeme, "PTR") == 0 ) {
|
|
|
|
|
param->literal_int = 1;
|
|
|
|
|
/* `TO` is already a verb -- FOR ... TO owns it -- so it arrives
|
|
|
|
|
as a command token rather than an identifier. */
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
|
|
|
}
|
|
|
|
|
snprintf(param->literal_string, sizeof(param->literal_string), "%s", word->lexeme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( tail == NULL ) {
|
|
|
|
|
arglist->right = param;
|
|
|
|
|
} else {
|
|
|
|
|
tail->next = param;
|
|
|
|
|
}
|
|
|
|
|
tail = param;
|
|
|
|
|
} while ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) );
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_RIGHT_PAREN),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected ) after the argument list");
|
|
|
|
|
*dest = arglist;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
|
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expression = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
akbasic_FunctionDef *fndef = NULL;
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected identifier");
|
|
|
|
|
|
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>
2026-08-01 13:05:32 -04:00
|
|
|
PASS(errctx, parse_def_parameters(parser, &arglist));
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
|
|
|
|
|
|
|
|
|
|
/* Uppercase the name: verbs and function names are case-insensitive. */
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strlen(identifier->identifier) < sizeof(fndef->name)),
|
|
|
|
|
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
|
|
|
|
|
for ( i = 0; i < strlen(identifier->identifier); i++ ) {
|
|
|
|
|
char c = identifier->identifier[i];
|
|
|
|
|
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
|
|
|
|
|
}
|
|
|
|
|
fndef->name[strlen(identifier->identifier)] = '\0';
|
|
|
|
|
|
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &expression));
|
|
|
|
|
PASS(errctx, akbasic_leaf_clone(expression, &fndef->leafpool, &fndef->expression));
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* No expression: the body is the lines that follow. Record where it
|
|
|
|
|
* starts and skip to RETURN so the definition itself does not execute
|
|
|
|
|
* the body.
|
|
|
|
|
*/
|
|
|
|
|
fndef->expression = NULL;
|
|
|
|
|
PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "RETURN"));
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist));
|
|
|
|
|
fndef->lineno = runtime->environment->lineno + 1;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "DEF", NULL));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* FOR ... TO .... [STEP ...]
|
|
|
|
|
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
|
|
|
|
*
|
|
|
|
|
* Sets up the loop's environment with the TO and STEP expressions and the first
|
|
|
|
|
* body line, then makes it the active environment. The FOR leaf itself carries
|
|
|
|
|
* the assignment.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
|
|
|
akbasic_ASTLeaf *assignment = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
akbasic_Environment *parent = runtime->environment;
|
|
|
|
|
akbasic_Environment *newenv = NULL;
|
|
|
|
|
int64_t firstline = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "TO"), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(assignment != NULL && akbasic_leaf_is_identifier(assignment->left)),
|
|
|
|
|
AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
|
|
|
|
|
|
|
|
firstline = parent->lineno + 1;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The loop body is scanned against the *parent* environment's token stream,
|
|
|
|
|
* so the new environment cannot become active until parsing is done. Parse
|
|
|
|
|
* TO and STEP first, then switch.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
|
|
|
|
newenv = runtime->environment;
|
|
|
|
|
runtime->environment = parent;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &newenv->forToLeaf));
|
|
|
|
|
|
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "STEP"), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf));
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* Dartmouth BASIC says not to infer a negative step: it is either given
|
|
|
|
|
* explicitly or assumed to be +1.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &newenv->forStepLeaf));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_literal_int(newenv->forStepLeaf, "1"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A NEXT already being awaited means this is an inner loop over the same variable. */
|
|
|
|
|
if ( strcmp(parent->waitingForCommand, "NEXT") == 0 ) {
|
|
|
|
|
newenv->forNextVariable = parent->forNextVariable;
|
|
|
|
|
}
|
|
|
|
|
newenv->loopFirstLine = firstline;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "FOR", assignment));
|
|
|
|
|
|
|
|
|
|
runtime->environment = newenv;
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* READ VARNAME [, ...]
|
|
|
|
|
* COMMAND ARGUMENTLIST
|
|
|
|
|
*
|
|
|
|
|
* The identifier leaves are deep-copied into the environment, because the DATA
|
|
|
|
|
* line that fills them is parsed later and will have overwritten the per-line
|
|
|
|
|
* leaf pool by then.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_read(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Environment *env = parser->runtime->environment;
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected identifier");
|
|
|
|
|
|
|
|
|
|
env->readLeafPool.next = 0;
|
|
|
|
|
expr = arglist->right;
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_LEAVES; i++ ) {
|
|
|
|
|
if ( expr == NULL ) {
|
|
|
|
|
env->readIdentifierLeaves[i] = NULL;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected identifier");
|
|
|
|
|
PASS(errctx, akbasic_leaf_clone(expr, &env->readLeafPool, &env->readIdentifierLeaves[i]));
|
|
|
|
|
/*
|
2026-07-31 11:58:15 -04:00
|
|
|
* A cloned identifier keeps its sibling chain, which for READ is the
|
|
|
|
|
* *next* identifier. Sever it so evaluating this leaf cannot walk into
|
|
|
|
|
* its sibling.
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
*/
|
2026-07-31 11:58:15 -04:00
|
|
|
env->readIdentifierLeaves[i]->next = NULL;
|
|
|
|
|
expr = expr->next;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
}
|
|
|
|
|
env->readReturnLine = env->lineno + 1;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "READ", arglist));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* DATA LITERAL [, ...]
|
|
|
|
|
* COMMAND ARGUMENTLIST
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_data(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected literal");
|
2026-07-31 11:58:15 -04:00
|
|
|
for ( expr = arglist->right; expr != NULL; expr = expr->next ) {
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_literal(expr), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected literal");
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "DATA", arglist));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_poke(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"POKE expected INTEGER, INTEGER");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "POKE", arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* IF relation THEN command [ELSE command]
|
|
|
|
|
* becomes BRANCH(expr=relation, left=then_command, right=else_command).
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *relation = NULL;
|
|
|
|
|
akbasic_ASTLeaf *then_command = NULL;
|
|
|
|
|
akbasic_ASTLeaf *else_command = NULL;
|
|
|
|
|
akbasic_ASTLeaf *branch = NULL;
|
|
|
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
|
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
/*
|
|
|
|
|
* Everything from here to the THEN is a condition, so a lone `=` in it is an
|
|
|
|
|
* equality test. Cleared immediately afterwards: the statement the THEN arm
|
|
|
|
|
* holds may well be an assignment.
|
|
|
|
|
*/
|
|
|
|
|
/*
|
|
|
|
|
* The whole expression, not just a relation. The reference parses one
|
|
|
|
|
* relation here, which is below AND and OR in the grammar chain -- so
|
|
|
|
|
* `IF A# = 5 AND B# = 3 THEN` stopped after the first comparison, left the
|
|
|
|
|
* AND unconsumed, and reported "Incomplete IF statement" against a line that
|
|
|
|
|
* is ordinary BASIC. A condition is an expression.
|
|
|
|
|
*/
|
|
|
|
|
parser->comparing = true;
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &relation));
|
|
|
|
|
parser->comparing = false;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Incomplete IF statement");
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "THEN"), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected IF ... THEN");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_command(parser, &then_command));
|
|
|
|
|
|
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
|
|
|
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "ELSE"), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected IF ... THEN ... ELSE ...");
|
|
|
|
|
PASS(errctx, akbasic_parser_command(parser, &else_command));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &branch));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_branch(branch, relation, then_command, else_command));
|
|
|
|
|
*dest = branch;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* INPUT "PROMPT" VARIABLE
|
|
|
|
|
* COMMAND EXPRESSION IDENTIFIER
|
|
|
|
|
*
|
|
|
|
|
* The prompt is hung off the identifier's .left, which is where the exec handler
|
|
|
|
|
* looks for it.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_input(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *promptexpr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
|
|
|
akbasic_ASTLeaf *command = NULL;
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* `INPUT #1, VAR` reads a line from a file channel rather than from the
|
|
|
|
|
* sink. Same spelling note as PRINT: the `#` is its own token and the space
|
|
|
|
|
* before it is required.
|
|
|
|
|
*/
|
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) {
|
|
|
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
|
|
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK);
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
|
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
|
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected INPUT #(channel), (variable)");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected a comma after INPUT #(channel)");
|
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &arglist->right->next));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(arglist->right->next),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected INPUT #(channel), (variable)");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "INPUT#", arglist));
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &promptexpr));
|
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected identifier");
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(command, "INPUT", identifier));
|
|
|
|
|
identifier->left = promptexpr;
|
|
|
|
|
*dest = command;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|