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 RENUMBER: move every line, and rewrite 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.
|
|
|
|
|
*
|
|
|
|
|
* The rewrite 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"`
|
|
|
|
|
* must come through untouched. That is the same statement-walking the label and
|
|
|
|
|
* DATA prescans do.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#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 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, rewriting each.
|
|
|
|
|
*
|
|
|
|
|
* `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 map Old line to new line.
|
|
|
|
|
* @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(const int64_t *map, const char **src, char **dest, const char *limit)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
const char *p = *src;
|
|
|
|
|
char *out = *dest;
|
|
|
|
|
|
|
|
|
|
for ( ;; ) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
written = snprintf(out, (size_t)(limit - out), "%" PRId64, mapped(map, line));
|
|
|
|
|
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 map Old line to new line.
|
|
|
|
|
* @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(const int64_t *map, const char *code, char *dest, size_t len)
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) {
|
|
|
|
|
verblen = strlen(BRANCH_VERBS[i]);
|
|
|
|
|
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(map, &p, &out, limit));
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
if ( strncasecmp(p, "COLLISION", 9) == 0 && !isalnum((unsigned char)p[9]) ) {
|
|
|
|
|
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(map, &p, &out, limit));
|
|
|
|
|
}
|
|
|
|
|
statementstart = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statementstart = false;
|
|
|
|
|
*out = *p;
|
|
|
|
|
out += 1;
|
|
|
|
|
p += 1;
|
|
|
|
|
}
|
|
|
|
|
*out = '\0';
|
|
|
|
|
(void)statementstart;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
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(map, obj->source[i].code,
|
|
|
|
|
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);
|
|
|
|
|
}
|