Files
akbasic/src/scanner.c

554 lines
18 KiB
C
Raw Normal View History

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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
/**
* @file scanner.c
* @brief Implements the line tokenizer.
*/
#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family could not report a conversion failure at all -- atoi("not a number") returned success with 0, turning four diagnosable errors into wrong answers. Its own note said what to do when that changed: "When it grows it, delete src/convert.c and switch the call sites over." 0.2.0 grew it, so it is gone. Six call sites go straight to the library now: both literal constructors in src/grammar.c, the line number in src/scanner.c, FunctionVAL in src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than aksl_atoll wherever a base is involved, because the ato* forms are base 10 and grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what makes trailing junk an error rather than a stopping point. The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes message text part of the acceptance contract, so that was checked against the corpus before touching anything: no golden file contained a conversion message, which is also why section 1.9 had been asking for one. Two exist now, so the next change to that text has to move a golden file, and one of them pins the reference's octal-literal defect (section 6 item 10) while it is still deliberately reproduced. tests/convert.c became tests/numeric_contract.c rather than being deleted with the code. The assertions did not stop being worth making when the wrapper went away -- they became assertions about a contract this port depends on and no longer owns, and a regression in it would make four things quietly return zero. Repoints the CI mutation job, which was bounded to src/convert.c and src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65. src/audio_tables.c was measured as a replacement second file and scored 64.7%: that is a real gap rather than a reason to skip it, since almost every survivor is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio state is actually zeroed -- the same gap this job's history records closing for symtab. Recorded in TODO.md so the file can earn its place back. One thing worth knowing before reading any score here, and now written down: the harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of other values produces no mutants and a high score over one says nothing about whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR entries against the datasheet anyway, because a hand-edit typo is the real failure mode there -- but that could not and did not move the mutation number, and claiming otherwise would be the kind of thing this file exists to stop. 72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and 97.8% function, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 10:28:04 -04:00
#include <akstdlib.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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
#include <akbasic/error.h>
#include <akbasic/scanner.h>
#include <akbasic/verbs.h>
akerr_ErrorContext *akbasic_scanner_zero(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scanner zero");
obj->current = 0;
obj->start = 0;
obj->hasError = false;
SUCCEED_RETURN(errctx);
}
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
/**
* @brief Is the cursor past the end of the line?
*
* The answer leaves through @p dest rather than the return value because
* measuring the line can fail, and a `bool` has nowhere to put that. Same shape
* as symtab.c's `probe`, and for the same reason. See libakstdlib #38.
*/
static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest)
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
{
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PREPARE_ERROR(errctx);
size_t linelen = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in is_at_end");
PASS(errctx, aksl_strlen(obj->line, &linelen));
*dest = (obj->current >= (int)linelen);
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
/**
* @brief The character under the cursor.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got)
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
{
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PREPARE_ERROR(errctx);
bool atend = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER,
"NULL argument in peek");
PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
*got = false;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
*dest = obj->line[obj->current];
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
*got = true;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
/**
* @brief The character one past the cursor.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
static akerr_ErrorContext *peek_next(akbasic_Runtime *obj, char *dest, bool *got)
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
{
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PREPARE_ERROR(errctx);
size_t linelen = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER,
"NULL argument in peek_next");
PASS(errctx, aksl_strlen(obj->line, &linelen));
if ( (obj->current + 1) >= (int)linelen ) {
*got = false;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
*dest = obj->line[obj->current + 1];
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
*got = true;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
/*
* The lexeme is the span [start, current), with two special cases the reference
* relies on: at end of line it runs to the end, and a zero-width span yields the
* single character at `start` -- unless we are closing a string literal, where
* an empty span really is the empty string.
*/
static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
size_t measured = 0;
int linelen = 0;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
int span = 0;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strlen(obj->line, &measured));
linelen = (int)measured;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
if ( obj->current == linelen ) {
span = linelen - obj->start;
} else if ( obj->start == obj->current ) {
if ( obj->tokentype == AKBASIC_TOK_LITERAL_STRING ) {
dest[0] = '\0';
SUCCEED_RETURN(errctx);
}
span = 1;
} else {
span = obj->current - obj->start;
}
FAIL_ZERO_RETURN(errctx, (span >= 0 && (size_t)span < len), AKBASIC_ERR_BOUNDS,
"Lexeme of %d characters exceeds the %zu character limit", span, len - 1);
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_memcpy(dest, obj->line + obj->start, (size_t)span));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
dest[span] = '\0';
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType token, const char *lexeme)
{
PREPARE_ERROR(errctx);
akbasic_Environment *env = obj->environment;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
size_t lexemelen = 0;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
FAIL_ZERO_RETURN(errctx, (env->nexttoken < AKBASIC_MAX_TOKENS), AKBASIC_ERR_BOUNDS,
"Line %" PRId64 " has more than %d tokens",
env->lineno, AKBASIC_MAX_TOKENS);
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strlen(lexeme, &lexemelen));
FAIL_ZERO_RETURN(errctx, (lexemelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
"Token lexeme exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1);
env->tokens[env->nexttoken].tokentype = token;
env->tokens[env->nexttoken].lineno = env->lineno;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strcpy(env->tokens[env->nexttoken].lexeme,
sizeof(env->tokens[env->nexttoken].lexeme), 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
env->nexttoken += 1;
SUCCEED_RETURN(errctx);
}
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
/**
* @brief Consume one more character when it matches, choosing between two token types.
* @param[out] matched Whether the character was consumed. The old `bool` return.
*
* On the chain below `peek`, so it reports the same way. See libakstdlib #38.
*/
static akerr_ErrorContext *match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType truetype,
akbasic_TokenType falsetype, bool *matched)
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
{
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PREPARE_ERROR(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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
char nc = '\0';
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
bool got = 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
FAIL_ZERO_RETURN(errctx, (obj != NULL && matched != NULL), AKERR_NULLPOINTER,
"NULL argument in match_next_char");
PASS(errctx, peek(obj, &nc, &got));
if ( !got ) {
/*
* Nothing left to peek at, so the operator is whatever it is on its
* own. The reference returns here *without* setting a type
* (basicscanner.go:272), which silently drops a comparison operator in
* the final column of a line: `A# =` produced one token, not two, and
* the parse error that followed pointed somewhere else entirely.
* TODO.md section 6 item 14.
*/
obj->tokentype = falsetype;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
*matched = false;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
if ( nc == cm ) {
obj->current += 1;
obj->tokentype = truetype;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
*matched = true;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
obj->tokentype = falsetype;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
*matched = false;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
static akerr_ErrorContext *match_string(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
char c = '\0';
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
bool atend = false;
bool got = 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
for ( ;; ) {
PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
if ( !got ) {
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE,
"UNTERMINATED STRING LITERAL\n"));
obj->hasError = true;
SUCCEED_RETURN(errctx);
}
if ( c == '"' ) {
break;
}
obj->current += 1;
}
obj->tokentype = AKBASIC_TOK_LITERAL_STRING;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *match_number(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
bool linenumber = (obj->environment->nexttoken == 0);
char lexeme[AKBASIC_MAX_LINE_LENGTH];
char c = '\0';
char nc = '\0';
int64_t lineno = 0;
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family could not report a conversion failure at all -- atoi("not a number") returned success with 0, turning four diagnosable errors into wrong answers. Its own note said what to do when that changed: "When it grows it, delete src/convert.c and switch the call sites over." 0.2.0 grew it, so it is gone. Six call sites go straight to the library now: both literal constructors in src/grammar.c, the line number in src/scanner.c, FunctionVAL in src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than aksl_atoll wherever a base is involved, because the ato* forms are base 10 and grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what makes trailing junk an error rather than a stopping point. The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes message text part of the acceptance contract, so that was checked against the corpus before touching anything: no golden file contained a conversion message, which is also why section 1.9 had been asking for one. Two exist now, so the next change to that text has to move a golden file, and one of them pins the reference's octal-literal defect (section 6 item 10) while it is still deliberately reproduced. tests/convert.c became tests/numeric_contract.c rather than being deleted with the code. The assertions did not stop being worth making when the wrapper went away -- they became assertions about a contract this port depends on and no longer owns, and a regression in it would make four things quietly return zero. Repoints the CI mutation job, which was bounded to src/convert.c and src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65. src/audio_tables.c was measured as a replacement second file and scored 64.7%: that is a real gap rather than a reason to skip it, since almost every survivor is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio state is actually zeroed -- the same gap this job's history records closing for symtab. Recorded in TODO.md so the file can earn its place back. One thing worth knowing before reading any score here, and now written down: the harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of other values produces no mutants and a high score over one says nothing about whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR entries against the datasheet anyway, because a hand-edit typo is the real failure mode there -- but that could not and did not move the mutation number, and claiming otherwise would be the kind of thing this file exists to stop. 72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and 97.8% function, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 10:28:04 -04:00
long long converted = 0;
bool hex = false;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
bool atend = false;
bool got = 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
obj->tokentype = AKBASIC_TOK_LITERAL_INT;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
for ( ;; ) {
PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
if ( c == '.' ) {
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, peek_next(obj, &nc, &got));
if ( !got || !isdigit((unsigned char)nc) ) {
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE,
"INVALID FLOATING POINT LITERAL\n"));
obj->hasError = true;
SUCCEED_RETURN(errctx);
}
obj->tokentype = AKBASIC_TOK_LITERAL_FLOAT;
} else if ( c == 'x' && !hex ) {
/*
* An 'x' in a number run. The reference lets it through and then
* stops at the first hex digit after it (basicscanner.go:318), so
* `0xff` lexed as `0x` followed by an identifier `ff`, the parser's
* base-16 branch was unreachable, and the README's hex support did
* not exist. TODO.md section 6 item 15. Once the 'x' is seen, keep
* going over hex digits.
*
* Accepted anywhere in the run rather than only after a leading `0`,
* which is the reference's rule and is worth keeping: it is what
* makes `1x2` one malformed token that the line-number conversion
* then diagnoses by name, instead of two tokens that fail somewhere
* less helpful.
*/
hex = true;
} else if ( hex ) {
if ( !isxdigit((unsigned char)c) ) {
break;
}
} else if ( !isdigit((unsigned char)c) ) {
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
break;
}
obj->current += 1;
}
if ( obj->tokentype == AKBASIC_TOK_LITERAL_INT && linenumber ) {
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
ATTEMPT {
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family could not report a conversion failure at all -- atoi("not a number") returned success with 0, turning four diagnosable errors into wrong answers. Its own note said what to do when that changed: "When it grows it, delete src/convert.c and switch the call sites over." 0.2.0 grew it, so it is gone. Six call sites go straight to the library now: both literal constructors in src/grammar.c, the line number in src/scanner.c, FunctionVAL in src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than aksl_atoll wherever a base is involved, because the ato* forms are base 10 and grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what makes trailing junk an error rather than a stopping point. The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes message text part of the acceptance contract, so that was checked against the corpus before touching anything: no golden file contained a conversion message, which is also why section 1.9 had been asking for one. Two exist now, so the next change to that text has to move a golden file, and one of them pins the reference's octal-literal defect (section 6 item 10) while it is still deliberately reproduced. tests/convert.c became tests/numeric_contract.c rather than being deleted with the code. The assertions did not stop being worth making when the wrapper went away -- they became assertions about a contract this port depends on and no longer owns, and a regression in it would make four things quietly return zero. Repoints the CI mutation job, which was bounded to src/convert.c and src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65. src/audio_tables.c was measured as a replacement second file and scored 64.7%: that is a real gap rather than a reason to skip it, since almost every survivor is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio state is actually zeroed -- the same gap this job's history records closing for symtab. Recorded in TODO.md so the file can earn its place back. One thing worth knowing before reading any score here, and now written down: the harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of other values produces no mutants and a high score over one says nothing about whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR entries against the datasheet anyway, because a hand-edit typo is the real failure mode there -- but that could not and did not move the mutation number, and claiming otherwise would be the kind of thing this file exists to stop. 72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and 97.8% function, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 10:28:04 -04:00
CATCH(errctx, aksl_atoll(lexeme, &converted));
lineno = (int64_t)converted;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
char message[AKBASIC_MAX_LINE_LENGTH + 32] = "INTEGER CONVERSION";
int written = 0;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
/*
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
* Reporting can itself fail if the sink is broken, and so can
* formatting the message. Neither leaves anything useful to do here --
* this block is already handling an error -- so both are ignored, the
* message keeps the initialiser above if the format fails, and the flag
* lets the next operation surface it.
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
*/
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
IGNORE(aksl_snprintf(&written, message, sizeof(message),
"INTEGER CONVERSION ON '%s'", 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
obj->hasError = true;
} FINISH(errctx, false);
if ( obj->hasError ) {
SUCCEED_RETURN(errctx);
}
obj->environment->lineno = lineno;
obj->tokentype = AKBASIC_TOK_LINE_NUMBER;
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
char lexeme[AKBASIC_MAX_LINE_LENGTH];
char basename[AKBASIC_MAX_LINE_LENGTH];
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
const akbasic_Verb *verb = NULL;
void *fndef = NULL;
bool userfunction = false;
char c = '\0';
size_t used = 0;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
bool atend = false;
bool got = 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
obj->tokentype = AKBASIC_TOK_IDENTIFIER;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
for ( ;; ) {
PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
if ( isdigit((unsigned char)c) || isalpha((unsigned char)c) ) {
obj->current += 1;
continue;
}
switch ( c ) {
case '@':
obj->tokentype = AKBASIC_TOK_IDENTIFIER_STRUCT;
obj->current += 1;
break;
case '$':
obj->tokentype = AKBASIC_TOK_IDENTIFIER_STRING;
obj->current += 1;
break;
case '%':
obj->tokentype = AKBASIC_TOK_IDENTIFIER_FLOAT;
obj->current += 1;
break;
case '#':
obj->tokentype = AKBASIC_TOK_IDENTIFIER_INT;
obj->current += 1;
break;
default:
break;
}
break;
}
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
/*
* The verb table is searched on the *base* name, with any type suffix
* stripped. The reference searches with the suffix still attached
* (basicscanner.go:349), so `PRINT$` misses every table, the "Reserved word
* in variable name" branch below is dead code, and `PRINT$ = 1` is quietly
* accepted as an ordinary string variable. TODO.md section 6 item 16.
*/
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strcpy(basename, sizeof(basename), lexeme));
PASS(errctx, aksl_strlen(basename, &used));
if ( obj->tokentype != AKBASIC_TOK_IDENTIFIER && used > 0 ) {
basename[used - 1] = '\0';
}
PASS(errctx, akbasic_verb_lookup(basename, &verb));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
ATTEMPT {
CATCH(errctx, akbasic_environment_get_function(obj->environment, lexeme, &fndef));
userfunction = true;
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
userfunction = false;
} FINISH(errctx, true);
if ( obj->tokentype == AKBASIC_TOK_IDENTIFIER ) {
if ( verb != NULL ) {
obj->tokentype = verb->tokentype;
} else if ( userfunction ) {
obj->tokentype = AKBASIC_TOK_FUNCTION;
}
} else if ( verb != NULL ) {
/*
* A suffixed identifier that collides with a verb or function name.
* PRINT$ is not a variable.
*/
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_SYNTAX,
"Reserved word in variable name\n"));
obj->hasError = true;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
char lexeme[AKBASIC_MAX_LINE_LENGTH];
char c = '\0';
bool done = false;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
bool atend = false;
bool matched = false;
size_t linelen = 0;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan");
FAIL_ZERO_RETURN(errctx, (line != NULL), AKERR_NULLPOINTER, "NULL line in scan");
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strlen(line, &linelen));
FAIL_ZERO_RETURN(errctx, (linelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
"Source line of %zu characters exceeds the %d character limit",
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
linelen, AKBASIC_MAX_LINE_LENGTH - 1);
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strcpy(obj->line, sizeof(obj->line), line));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
PASS(errctx, akbasic_environment_zero_parser(obj->environment));
obj->current = 0;
obj->start = 0;
obj->hasError = false;
Give a loaded line a number when it arrives without one A script written against LABEL and GOTO NAME never names a line number, so the numbers it had to carry were decoration. akbasic_runtime_load(), RUNSTREAM and DLOAD now file an unnumbered line one slot after the last one filed; a numbered line is filed under its number and moves the cursor, so the two mix. akbasic_runtime_file_line() is the one implementation of that rule, so the three paths cannot drift. The prompt is untouched. A line typed without a number is still direct mode and still runs now -- that is the only thing separating program text from a statement at a REPL, and it is why this is a loading feature. What this replaces was silent data loss: an unnumbered line was filed under the cursor unchanged, on top of the line before it. A blank line therefore erased whatever preceded it, and RUNSTREAM did not skip blank lines the way the other two paths did. That moves one golden file, and the reference had the same defect. language/arithmetic/integer.bas has four PRINT statements, an expectation with three values, and a trailing blank line that erased 40 PRINT 4 - 2 before the program ran. The expectation is now 4 4 2 2. tests/reference/README.md records the divergence and TODO.md section 5 item 63 says why. akbasic_SourceLine grows a `numbered` flag so an assigned number can be told from a written one. RENUMBER sets it on every line it touches; NEW, DELETE and DLOAD clear it. hadlinenumber moves to akbasic_scanner_scan(), so it always describes the line just scanned rather than only the REPL's. Two lines carrying the same written number still keep the last, as they always have. That is a separate decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 16:33:27 -04:00
/*
* Cleared here rather than by each caller, so the flag always describes the
* line this call just scanned. It used to be cleared only in
* process_line_repl(); the loading paths never touched it, which was
* harmless while they ignored it and is not now that they decide a line's
* number by it.
*/
obj->hadlinenumber = 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
for ( ;; ) {
PASS(errctx, is_at_end(obj, &atend));
if ( atend || done ) {
break;
}
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
c = obj->line[obj->current];
obj->current += 1;
switch ( c ) {
case '@': obj->tokentype = AKBASIC_TOK_ATSYMBOL; break;
case '^': obj->tokentype = AKBASIC_TOK_CARAT; break;
case '(': obj->tokentype = AKBASIC_TOK_LEFT_PAREN; break;
case ')': obj->tokentype = AKBASIC_TOK_RIGHT_PAREN; break;
case '+': obj->tokentype = AKBASIC_TOK_PLUS; break;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 11:39:47 -04:00
/*
* `->` reaches a field through a pointer, so a minus has to look ahead
* one character before it can call itself a minus. Nothing else in the
* language begins `->`, and `A# - >` is not an expression, so there is
* no spelling this makes ambiguous.
*/
case '-':
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_ARROW, AKBASIC_TOK_MINUS, &matched));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 11:39:47 -04:00
break;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
case '/': obj->tokentype = AKBASIC_TOK_LEFT_SLASH; break;
case '*': obj->tokentype = AKBASIC_TOK_STAR; break;
case ',': obj->tokentype = AKBASIC_TOK_COMMA; break;
case ':': obj->tokentype = AKBASIC_TOK_COLON; break;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 11:39:47 -04:00
/*
* A field separator. It only reaches this switch when it is *not* part
* of a number: match_number() is entered on a digit and consumes its own
* decimal point, so `1.5` never gets here and `P@.X#` always does.
*/
case '.': obj->tokentype = AKBASIC_TOK_DOT; break;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:37 -04:00
/*
* MOVSPR's two separators. Both were UNKNOWN TOKEN until sprites, so
* scanning them breaks nothing -- and a `#` only reaches this switch
* when it is not an identifier's type suffix, which match_identifier
* consumes before returning.
*/
case ';': obj->tokentype = AKBASIC_TOK_SEMICOLON; break;
case '#': obj->tokentype = AKBASIC_TOK_HASHMARK; break;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
case '[': obj->tokentype = AKBASIC_TOK_LEFT_SQUAREBRACKET; break;
case ']': obj->tokentype = AKBASIC_TOK_RIGHT_SQUAREBRACKET; break;
case '=':
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_EQUAL, AKBASIC_TOK_ASSIGNMENT, &matched));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
break;
case '<':
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_LESS_THAN_EQUAL,
AKBASIC_TOK_LESS_THAN, &matched));
if ( !matched ) {
PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_NOT_EQUAL,
AKBASIC_TOK_LESS_THAN, &matched));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
break;
case '>':
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_GREATER_THAN_EQUAL,
AKBASIC_TOK_GREATER_THAN, &matched));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
break;
case '"':
obj->start = obj->current;
PASS(errctx, match_string(obj));
break;
case '\t':
case ' ':
obj->start = obj->current;
break;
case '\r':
case '\n':
done = true;
break;
default:
if ( isdigit((unsigned char)c) ) {
PASS(errctx, match_number(obj));
} else if ( isalpha((unsigned char)c) ) {
PASS(errctx, match_identifier(obj));
} else {
char message[AKBASIC_MAX_LINE_LENGTH];
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
int written = 0;
PASS(errctx, aksl_snprintf(&written, message, sizeof(message),
"UNKNOWN TOKEN %c\n", c));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
obj->hasError = true;
obj->start = obj->current;
}
break;
}
if ( done ) {
break;
}
if ( obj->tokentype != AKBASIC_TOK_UNDEFINED && !obj->hasError ) {
if ( obj->tokentype == AKBASIC_TOK_REM ) {
/* Everything after REM is a comment. Stop, keeping the line intact. */
break;
} else if ( obj->tokentype == AKBASIC_TOK_LINE_NUMBER ) {
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:37 -04:00
obj->hadlinenumber = true;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
/*
* The line number is not kept as a token. Rewrite the line to
* everything after it, minus leading spaces, and restart the
* cursor -- the REPL reads the rewritten line back out and
* stores *that* as the program text.
*/
int skip = obj->current;
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
size_t tail = 0;
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
while ( obj->line[skip] == ' ' ) {
skip += 1;
}
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strlen(obj->line + skip, &tail));
PASS(errctx, aksl_memmove(obj->line, obj->line + skip, tail + 1));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
obj->current = 0;
} else {
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
PASS(errctx, add_token(obj, obj->tokentype, lexeme));
if ( obj->tokentype == AKBASIC_TOK_LITERAL_STRING ) {
/* Scanning stopped on the closing quote; step past it. */
obj->current += 1;
}
}
obj->tokentype = AKBASIC_TOK_UNDEFINED;
obj->start = obj->current;
}
}
if ( dest != NULL ) {
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strlen(obj->line, &linelen));
FAIL_ZERO_RETURN(errctx, (linelen < len), AKBASIC_ERR_BOUNDS,
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
"Scanned line does not fit the caller's buffer");
Port onto libakstdlib 2b79aca and convert the eight bool predicates akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
PASS(errctx, aksl_strcpy(dest, len, obj->line));
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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
}
SUCCEED_RETURN(errctx);
}