Files
akbasic/src/renumber.c

462 lines
16 KiB
C
Raw Normal View History

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
/**
* @file renumber.c
* @brief Walking a program's branch targets, and the two things that do it.
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
*
* **RENUMBER** moves every line and rewrites every reference to one. Moving the
* lines is easy -- source is filed under its line number in `source[]`, so it is
* a permutation. Rewriting every `GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP` and
* `COLLISION` target to match is the job, and it is why this was deferred rather
* than written early: a `RENUMBER` that moved lines without fixing their
* references would silently break every branch in the program, which is worse
* than not having the verb at all.
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
*
* **akbasic_runtime_check_targets()** walks the same targets without moving
* anything, refusing a program that branches by number to a line the program
* never numbered. It shares this file because it needs exactly the same walk,
* and a second copy of it would drift.
*
* The walk is textual, and the only thing it has to understand about the rest of
* the language is where a string literal starts and stops -- `PRINT "GOTO 10"`
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
* must come through untouched. That is the same statement-walking the label and
* DATA prescans do. What the two callers differ in is one function pointer:
* given a target, say what text should stand in its place, and raise if it is not
* acceptable.
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
*/
#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <akerror.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
/**
* @brief Verbs whose numeric arguments name a line.
*
* `THEN` and `ELSE` are deliberately absent. Commodore BASIC lets `IF X THEN 100`
* stand for `THEN GOTO 100`; this dialect does not -- the corpus writes
* `THEN GOTO 100` -- so a number after `THEN` here is an expression, not a target.
*
* `LIST` and `DELETE` are absent too, and for a different reason: their
* arguments are ranges typed at a prompt, not references stored in a program.
* Renumbering a `DELETE` inside a listing would be rewriting something nobody
* meant as a branch.
*/
static const char *BRANCH_VERBS[] = { "GOTO", "GOSUB", "RUN", "RESTORE", "TRAP" };
/** @brief How many entries #BRANCH_VERBS has. */
#define BRANCH_VERB_COUNT ((int)(sizeof(BRANCH_VERBS) / sizeof(BRANCH_VERBS[0])))
/**
* @brief What a walk does with each numeric branch target it finds.
*
* The one thing RENUMBER and the target check differ in. `visit` is handed a
* target and writes into @p dest the text that should stand in its place --
* the new number for RENUMBER, the same number for a check -- and may raise to
* refuse the line outright.
*/
typedef struct akbasic_TargetWalk akbasic_TargetWalk;
struct akbasic_TargetWalk
{
void *self;
akerr_ErrorContext *(*visit)(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len);
};
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
/**
* @brief Where a line moved to, or the line itself when it did not move.
*
* A target naming a line that does not exist is left alone rather than
* rewritten: `GOTO 9999` in a program with no line 9999 is already broken, and
* inventing a destination for it would hide that.
*/
static int64_t mapped(const int64_t *map, int64_t line)
{
if ( line < 0 || line >= AKBASIC_MAX_SOURCE_LINES ) {
return line;
}
return (map[line] >= 0 ? map[line] : line);
}
/**
* @brief Copy a run of comma-separated line numbers, visiting each.
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
*
* `ON X GOTO 100, 200, 300` is why this takes a list rather than one number: the
* targets follow the `GOTO`, so handling `GOTO` handles `ON` for free.
*
* @param walk What to substitute for each target, and whether to allow it.
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
* @param src Reads from here; advanced past what was consumed.
* @param dest Writes to here; advanced past what was written.
* @param limit One past the last byte @p dest may write.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit.
*/
static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char **src, char **dest, const char *limit)
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
{
PREPARE_ERROR(errctx);
const char *p = *src;
char *out = *dest;
for ( ;; ) {
char replacement[32];
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
int64_t line = 0;
int written = 0;
while ( *p == ' ' || *p == '\t' ) {
FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit");
*out = *p;
out += 1;
p += 1;
}
if ( !isdigit((unsigned char)*p) ) {
/*
* Not a number: a label, an expression, or nothing at all. Labels are
* the reason RENUMBER is survivable in the first place -- a program
* written with `GOTO DONE` needs no rewriting and cannot be broken by
* one.
*/
break;
}
while ( isdigit((unsigned char)*p) ) {
line = (line * 10) + (*p - '0');
p += 1;
}
PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement)));
written = snprintf(out, (size_t)(limit - out), "%s", replacement);
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
FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit");
out += written;
/*
* A comma continues the list; anything else ends it. The spaces are
* skipped to find out which, and put back if the answer is "ends" --
* without that, `THEN GOTO 9 ELSE ...` came back as `THEN GOTO 30ELSE`.
*/
{
const char *afterdigits = p;
while ( *p == ' ' || *p == '\t' ) {
p += 1;
}
if ( *p != ',' ) {
p = afterdigits;
break;
}
}
FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit");
*out = ',';
out += 1;
p += 1;
}
*src = p;
*dest = out;
SUCCEED_RETURN(errctx);
}
/**
* @brief Rewrite every branch target in one line of source.
*
* @param walk What to substitute for each target, and whether to allow it.
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
* @param code The line to read.
* @param dest Where the rewritten line goes.
* @param len Capacity of @p dest.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit.
*/
static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *code, char *dest, size_t len)
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
{
PREPARE_ERROR(errctx);
const char *p = code;
char *out = dest;
const char *limit = dest + len - 1;
bool statementstart = true;
bool instring = false;
int i = 0;
while ( *p != '\0' ) {
size_t verblen = 0;
bool matched = false;
FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit");
if ( instring ) {
instring = (*p != '"');
*out = *p;
out += 1;
p += 1;
continue;
}
if ( *p == '"' ) {
/* A string literal is copied through untouched: `PRINT "GOTO 10"`. */
instring = true;
statementstart = false;
*out = *p;
out += 1;
p += 1;
continue;
}
if ( *p == ':' ) {
statementstart = true;
*out = *p;
out += 1;
p += 1;
continue;
}
if ( *p == ' ' || *p == '\t' ) {
*out = *p;
out += 1;
p += 1;
continue;
}
Give the characters breakout its bricks as collision geometry `HITTEST`, `XBRICK` and `YBRICK` are gone -- about forty lines that divided pixels by cell sizes to recover a grid index, tested the ball's leading edge rather than its box, and could miss a brick clipped at the corner by seven pixels' worth of ball. In their place: sixty `SOLID` rectangles registered as the wall is built, `COLLISION 2, BRICKHIT`, and a fifteen-line handler. The handler reads better than what it replaces because it asks rather than derives. `RCOLLISION(1, 1)` is which brick -- the id is the array index plus one, so nothing is looked up. Fields 2, 3 and 4 are the way out and how far, so the ball is pushed exactly clear instead of being restored to a remembered `OX#`/`OY#`. Field 7 is which axis to reverse, which `TESTCELL` in the other game computes by hand from an overlap rectangle. `MOVEBAL` loses its two brick calls and its position backup. `KILLBR` retires the rectangle in the same breath as clearing the array element, so the next frame cannot hit a brick that is no longer drawn. **A latent defect in the target prescan had to be fixed first, and `RCOLLISION` is the first name in the language to reach it.** `src/renumber.c` walks a line character by character looking for `GOTO`, `GOSUB`, `COLLISION` and the rest, and checked only the character *after* a match -- so `RCOLLISION(1, 1)` found `COLLISION` at its second character, read the `(1,` that followed as a handler line number, and refused the whole program with "branch to line 1, which the program did not number", naming a line that contains no branch at all. It now requires a word boundary on both sides. The comment there was already right that the trailing check protects `GOTOX#`; nothing protected `XGOTO#`. `tests/unnumbered.c` covers all three shapes and TODO.md section 6 item 42 records it. The game runs ninety seconds headless with no error line and the attract mode scores 1320, so bricks are being found and broken through the new path. Chapter 17 is not updated yet -- that is the other half of section 6 item 39, and it is a bigger edit than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 11:17:34 -04:00
/*
* **Both ends, not just the trailing one.** The check below used to look
* only at the character *after* a match, so a name ending in a branch
* verb matched inside itself: `RCOLLISION(1, 1)` found `COLLISION` at its
* second character, read the `(1,` that followed as a handler argument,
* and refused the whole program with "branch to line 1, which the program
* did not number" -- naming a line that contains no branch at all.
*
* The comment below is right that the trailing check is what protects
* `GOTOX#`. Nothing protected `XGOTO#`, and `RCOLLISION` is the first
* name in the language to reach it.
*/
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
for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) {
verblen = strlen(BRANCH_VERBS[i]);
Give the characters breakout its bricks as collision geometry `HITTEST`, `XBRICK` and `YBRICK` are gone -- about forty lines that divided pixels by cell sizes to recover a grid index, tested the ball's leading edge rather than its box, and could miss a brick clipped at the corner by seven pixels' worth of ball. In their place: sixty `SOLID` rectangles registered as the wall is built, `COLLISION 2, BRICKHIT`, and a fifteen-line handler. The handler reads better than what it replaces because it asks rather than derives. `RCOLLISION(1, 1)` is which brick -- the id is the array index plus one, so nothing is looked up. Fields 2, 3 and 4 are the way out and how far, so the ball is pushed exactly clear instead of being restored to a remembered `OX#`/`OY#`. Field 7 is which axis to reverse, which `TESTCELL` in the other game computes by hand from an overlap rectangle. `MOVEBAL` loses its two brick calls and its position backup. `KILLBR` retires the rectangle in the same breath as clearing the array element, so the next frame cannot hit a brick that is no longer drawn. **A latent defect in the target prescan had to be fixed first, and `RCOLLISION` is the first name in the language to reach it.** `src/renumber.c` walks a line character by character looking for `GOTO`, `GOSUB`, `COLLISION` and the rest, and checked only the character *after* a match -- so `RCOLLISION(1, 1)` found `COLLISION` at its second character, read the `(1,` that followed as a handler line number, and refused the whole program with "branch to line 1, which the program did not number", naming a line that contains no branch at all. It now requires a word boundary on both sides. The comment there was already right that the trailing check protects `GOTOX#`; nothing protected `XGOTO#`. `tests/unnumbered.c` covers all three shapes and TODO.md section 6 item 42 records it. The game runs ninety seconds headless with no error line and the attract mode scores 1320, so bricks are being found and broken through the new path. Chapter 17 is not updated yet -- that is the other half of section 6 item 39, and it is a bigger edit than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 11:17:34 -04:00
if ( p > code && isalnum((unsigned char)p[-1]) ) {
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
if ( strncasecmp(p, BRANCH_VERBS[i], verblen) == 0 &&
!isalnum((unsigned char)p[verblen]) ) {
matched = true;
break;
}
}
/*
* A branch verb may appear anywhere a statement may, and `GOTO` also
* follows `THEN`, `ELSE` and `ON X` -- none of which is a statement
* start. So the match is not gated on statementstart; what protects
* `A$ = "GOTO"` is the string check above, and what protects a variable
* called `GOTOX#` is the alphanumeric check here.
*/
if ( matched ) {
FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest),
AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit");
memcpy(out, p, verblen);
out += verblen;
p += verblen;
PASS(errctx, rewrite_targets(walk, &p, &out, limit));
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
statementstart = false;
continue;
}
/*
* COLLISION's handler is its *second* argument, so the list rewrite
* cannot be pointed at it directly. Copy the type and the comma, then
* rewrite what follows.
*/
Give the characters breakout its bricks as collision geometry `HITTEST`, `XBRICK` and `YBRICK` are gone -- about forty lines that divided pixels by cell sizes to recover a grid index, tested the ball's leading edge rather than its box, and could miss a brick clipped at the corner by seven pixels' worth of ball. In their place: sixty `SOLID` rectangles registered as the wall is built, `COLLISION 2, BRICKHIT`, and a fifteen-line handler. The handler reads better than what it replaces because it asks rather than derives. `RCOLLISION(1, 1)` is which brick -- the id is the array index plus one, so nothing is looked up. Fields 2, 3 and 4 are the way out and how far, so the ball is pushed exactly clear instead of being restored to a remembered `OX#`/`OY#`. Field 7 is which axis to reverse, which `TESTCELL` in the other game computes by hand from an overlap rectangle. `MOVEBAL` loses its two brick calls and its position backup. `KILLBR` retires the rectangle in the same breath as clearing the array element, so the next frame cannot hit a brick that is no longer drawn. **A latent defect in the target prescan had to be fixed first, and `RCOLLISION` is the first name in the language to reach it.** `src/renumber.c` walks a line character by character looking for `GOTO`, `GOSUB`, `COLLISION` and the rest, and checked only the character *after* a match -- so `RCOLLISION(1, 1)` found `COLLISION` at its second character, read the `(1,` that followed as a handler line number, and refused the whole program with "branch to line 1, which the program did not number", naming a line that contains no branch at all. It now requires a word boundary on both sides. The comment there was already right that the trailing check protects `GOTOX#`; nothing protected `XGOTO#`. `tests/unnumbered.c` covers all three shapes and TODO.md section 6 item 42 records it. The game runs ninety seconds headless with no error line and the attract mode scores 1320, so bricks are being found and broken through the new path. Chapter 17 is not updated yet -- that is the other half of section 6 item 39, and it is a bigger edit than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 11:17:34 -04:00
if ( strncasecmp(p, "COLLISION", 9) == 0 && !isalnum((unsigned char)p[9])
&& !(p > code && isalnum((unsigned char)p[-1])) ) {
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
memcpy(out, p, 9);
out += 9;
p += 9;
while ( *p != '\0' && *p != ',' && *p != ':' ) {
FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit");
*out = *p;
out += 1;
p += 1;
}
if ( *p == ',' ) {
*out = ',';
out += 1;
p += 1;
PASS(errctx, rewrite_targets(walk, &p, &out, limit));
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
}
statementstart = false;
continue;
}
statementstart = false;
*out = *p;
out += 1;
p += 1;
}
*out = '\0';
(void)statementstart;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------- RENUMBER -- */
/** @brief RENUMBER's visitor: substitute the number the line moved to. */
static akerr_ErrorContext *visit_renumber(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
const int64_t *map = (const int64_t *)walk->self;
snprintf(dest, len, "%" PRId64, mapped(map, target));
SUCCEED_RETURN(errctx);
}
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:37 -04:00
akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int64_t increment, int64_t oldstart)
{
PREPARE_ERROR(errctx);
static int64_t map[AKBASIC_MAX_SOURCE_LINES];
static akbasic_SourceLine rewritten[AKBASIC_MAX_SOURCE_LINES];
akbasic_TargetWalk walk = { map, visit_renumber };
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
int64_t next = newstart;
int64_t i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in renumber");
FAIL_ZERO_RETURN(errctx, (increment > 0), AKBASIC_ERR_VALUE,
"RENUMBER's increment must be positive, not %" PRId64, increment);
FAIL_ZERO_RETURN(errctx, (newstart > 0), AKBASIC_ERR_VALUE,
"RENUMBER cannot start at line %" PRId64, newstart);
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
map[i] = -1;
}
/*
* Build the whole map before touching anything. A rewrite needs to know
* where *every* line ended up, including ones it has not reached yet --
* a backward `GOTO` is as common as a forward one.
*/
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' || i < oldstart ) {
continue;
}
FAIL_ZERO_RETURN(errctx, (next < AKBASIC_MAX_SOURCE_LINES), AKBASIC_ERR_BOUNDS,
"RENUMBER: line %" PRId64 " would become %" PRId64 ", past the %d line limit",
i, next, AKBASIC_MAX_SOURCE_LINES - 1);
/*
* A renumbered line must not land on a kept one. Refused rather than
* overwritten: losing a line to a renumbering is not recoverable.
*/
FAIL_ZERO_RETURN(errctx, (next >= oldstart || obj->source[next].code[0] == '\0'),
AKBASIC_ERR_VALUE,
"RENUMBER: line %" PRId64 " would become %" PRId64 ", which already exists",
i, next);
map[i] = next;
next += increment;
}
memset(rewritten, 0, sizeof(rewritten));
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
int64_t target = 0;
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
/*
* Every line is rewritten, not just the moved ones: a line before
* `oldstart` can branch into the region that moved.
*/
target = mapped(map, i);
PASS(errctx, rewrite_line(&walk, obj->source[i].code,
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
rewritten[target].code, sizeof(rewritten[target].code)));
rewritten[target].lineno = target;
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
/*
* Every line comes out numbered, whether or not it went in that way.
* Asking for numbers is what RENUMBER is, and a program that has been
* through it can be branched into by number -- which is the whole point
* of running it over source that arrived without any.
*/
rewritten[target].numbered = true;
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
}
memcpy(obj->source, rewritten, sizeof(obj->source));
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------- the target prescan -- */
/** @brief What visit_check() needs to name the line it refused. */
typedef struct
{
const akbasic_Runtime *runtime;
int64_t lineno;
} CheckState;
/**
* @brief The prescan's visitor: allow the target, or refuse the program.
*
* A number that names a line the program never numbered is refused. In a script
* written without line numbers `GOTO 100` finds the hundredth line and branches
* there -- plausible, silent, and wrong, which is the worst kind of defect to be
* handed at run time.
*
* A target naming an *empty* slot is allowed through, for the same reason
* mapped() leaves one alone: `GOTO 9999` in a program with no line 9999 is
* already broken, and refusing it here would be inventing a rule about lines
* that do not exist. The substitution is the number itself, because nothing is
* being rewritten.
*/
static akerr_ErrorContext *visit_check(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
CheckState *state = (CheckState *)walk->self;
snprintf(dest, len, "%" PRId64, target);
if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) {
FAIL_NONZERO_RETURN(errctx,
(state->runtime->source[target].code[0] != '\0'
&& !state->runtime->source[target].numbered),
AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": branch to line %" PRId64
", which the program did not number. Branch by LABEL, or RENUMBER first",
state->lineno, target);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_runtime_check_targets(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
/*
* Static for the same reason akbasic_renumber()'s buffers are: a line's worth
* of rewrite does not belong on a default stack alongside the rest of a
* step(). Nothing is read back out of it -- the walk needs somewhere to put
* the text it would have written, and this is it.
*/
static char discard[AKBASIC_MAX_LINE_LENGTH * 2];
CheckState state = { NULL, 0 };
akbasic_TargetWalk walk = { &state, visit_check };
int64_t entry = 0;
int64_t i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in check_targets");
state.runtime = obj;
entry = obj->environment->lineno;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
state.lineno = i;
/*
* Point the runtime at the line being walked, so the `? N :` prefix
* akbasic_runtime_error() builds from environment->lineno names the
* offending line rather than whichever one the loader stopped on. The
* message repeats the number because the other three prescans do not do
* this and get it wrong -- see TODO.md.
*
* PASS inside the loop, never CATCH: CATCH expands to a break, which
* would leave this loop rather than an enclosing ATTEMPT. That also means
* the cursor is deliberately *not* restored on the failure path, which is
* the whole point of setting it.
*/
obj->environment->lineno = i;
PASS(errctx, rewrite_line(&walk, obj->source[i].code, discard, sizeof(discard)));
}
/* Nothing was refused, so leave the cursor as the caller had it. */
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx);
}