Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
/**
|
|
|
|
|
* @file runtime_commands.c
|
|
|
|
|
* @brief The verb implementations.
|
|
|
|
|
*
|
|
|
|
|
* Ported from basicruntime_commands.go. Every handler has the signature the
|
|
|
|
|
* dispatch table demands, and every one returns its result through `dest`.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
#include <akstdlib.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
#include <akbasic/scanner.h>
|
|
|
|
|
|
|
|
|
|
#include "verbs.h"
|
|
|
|
|
|
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
|
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
|
|
|
do { \
|
|
|
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
|
|
|
} while ( 0 )
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_let(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* LET is not required in this dialect or in Commodore BASIC 7.0. Assignment
|
|
|
|
|
* is part of expression evaluation, so there is nothing for LET to do.
|
|
|
|
|
*/
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_def(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
/* The parse handler already installed the function. */
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_print(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char rendered[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
|
|
|
|
|
if ( expr->right == NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_println(obj, ""));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, dest));
|
|
|
|
|
PASS(errctx, akbasic_value_to_string(*dest, rendered, sizeof(rendered)));
|
|
|
|
|
PASS(errctx, akbasic_runtime_println(obj, rendered));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_goto(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *target = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"Expected GOTO (line number or label)");
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &target));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (target->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected integer");
|
|
|
|
|
obj->environment->nextline = target->intval;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_gosub(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *target = NULL;
|
|
|
|
|
int64_t returnline = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"Expected GOSUB (line number or label)");
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &target));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (target->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected integer");
|
|
|
|
|
returnline = obj->environment->lineno + 1;
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
|
|
|
|
obj->environment->gosubReturnLine = returnline;
|
|
|
|
|
obj->environment->nextline = target->intval;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *result = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* A RETURN reached while skipping forward to one is the end of a DEF body,
|
|
|
|
|
* not a subroutine return. Stop waiting and carry on.
|
|
|
|
|
*/
|
|
|
|
|
if ( akbasic_environment_is_waiting_for(obj->environment, "RETURN") ) {
|
|
|
|
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "RETURN"));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
|
|
|
|
"RETURN outside the context of GOSUB");
|
|
|
|
|
|
|
|
|
|
if ( expr != NULL && expr->right != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
|
|
|
|
} else {
|
|
|
|
|
result = &obj->staticTrueValue;
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
|
|
|
"RETURN from an orphaned environment");
|
|
|
|
|
obj->environment->parent->nextline = obj->environment->gosubReturnLine;
|
|
|
|
|
PASS(errctx, akbasic_value_clone(result, &obj->environment->returnValue));
|
|
|
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
|
|
|
*dest = result;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_stop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_REPL));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_quit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* Sets a mode and returns. Nothing in this library calls exit() -- the
|
|
|
|
|
* driver's main() decides what quitting means, and an embedding game may
|
|
|
|
|
* decide it means something else entirely.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_QUIT));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_run(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *target = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
obj->environment->nextline = 0;
|
|
|
|
|
if ( expr != NULL && expr->right != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &target));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (target->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected RUN (line number)");
|
|
|
|
|
obj->environment->nextline = target->intval;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_label(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"Expected LABEL IDENTIFIER");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr->right), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected identifier");
|
|
|
|
|
PASS(errctx, akbasic_environment_set_label(obj->environment, expr->right->identifier,
|
|
|
|
|
obj->environment->lineno));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_auto(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *step = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
if ( expr == NULL || expr->right == NULL ) {
|
|
|
|
|
obj->autoLineNumber = 0;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &step));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (step->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected AUTO (integer)");
|
|
|
|
|
obj->autoLineNumber = step->intval;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dim(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
akbasic_ASTLeaf *walk = NULL;
|
|
|
|
|
akbasic_Value *size = NULL;
|
|
|
|
|
int64_t sizes[AKBASIC_MAX_ARRAY_DEPTH];
|
|
|
|
|
int sizecount = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(expr != NULL && expr->right != NULL && expr->right->right != NULL &&
|
|
|
|
|
expr->right->right->leaftype == AKBASIC_LEAF_ARGUMENTLIST &&
|
|
|
|
|
expr->right->right->operator_ == AKBASIC_TOK_ARRAY_SUBSCRIPT &&
|
|
|
|
|
akbasic_leaf_is_identifier(expr->right)),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected DIM IDENTIFIER(DIMENSIONS, ...)");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &variable));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"Unable to get variable for identifier %s", expr->right->identifier);
|
|
|
|
|
|
|
|
|
|
for ( walk = expr->right->right->right; walk != NULL; walk = walk->right ) {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (sizecount < AKBASIC_MAX_ARRAY_DEPTH), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"More than %d array dimensions", AKBASIC_MAX_ARRAY_DEPTH);
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, walk, &size));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (size->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Array dimensions must evaluate to integer");
|
|
|
|
|
sizes[sizecount] = size->intval;
|
|
|
|
|
sizecount += 1;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_variable_init(variable, &obj->valuepool, sizes, sizecount));
|
|
|
|
|
PASS(errctx, akbasic_variable_zero(variable));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* POKE and PEEK write and read a single byte at a caller-supplied address.
|
|
|
|
|
*
|
|
|
|
|
* The pointer.bas golden case sets A# = 255 and expects PEEK(POINTER(A#)) to be
|
|
|
|
|
* 255, which holds only on a little-endian machine: A# is an int64_t and the
|
|
|
|
|
* low byte has to come first. Stated here rather than discovered later.
|
|
|
|
|
*/
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_poke(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
akbasic_Value *addrval = NULL;
|
|
|
|
|
akbasic_Value *byteval = NULL;
|
|
|
|
|
uint8_t *target = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
|
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "POKE expected INTEGER, INTEGER");
|
|
|
|
|
|
|
|
|
|
/* The address must be the live value, not a copy of it. */
|
|
|
|
|
obj->eval_clone_identifiers = false;
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_runtime_evaluate(obj, arg, &addrval));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
obj->eval_clone_identifiers = true;
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (addrval->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"POKE expected INTEGER, INTEGER");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(arg->right != NULL &&
|
|
|
|
|
(arg->right->leaftype == AKBASIC_LEAF_LITERAL_INT ||
|
|
|
|
|
arg->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT)),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "POKE expected INTEGER, INTEGER");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (addrval->intval != 0), AKBASIC_ERR_VALUE,
|
|
|
|
|
"POKE got NIL pointer or uninitialized variable");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg->right, &byteval));
|
|
|
|
|
target = (uint8_t *)(uintptr_t)addrval->intval;
|
|
|
|
|
*target = (uint8_t)byteval->intval;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
|
|
|
akbasic_Value *prompt = NULL;
|
|
|
|
|
akbasic_Value *entered = NULL;
|
|
|
|
|
akbasic_Value *unused = NULL;
|
|
|
|
|
char rendered[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
|
char buffer[AKBASIC_MAX_LINE_LENGTH];
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
long long converted = 0;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
bool eof = false;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"Expected INPUT \"PROMPT\" IDENTIFIER");
|
|
|
|
|
identifier = expr->right;
|
|
|
|
|
|
|
|
|
|
if ( identifier->left != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, identifier->left, &prompt));
|
|
|
|
|
PASS(errctx, akbasic_value_to_string(prompt, rendered, sizeof(rendered)));
|
|
|
|
|
PASS(errctx, akbasic_runtime_write(obj, rendered));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->sink->readline(obj->sink, buffer, sizeof(buffer), &eof));
|
|
|
|
|
if ( eof ) {
|
|
|
|
|
obj->inputEof = true;
|
|
|
|
|
buffer[0] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &entered));
|
|
|
|
|
PASS(errctx, akbasic_value_zero(entered));
|
|
|
|
|
|
|
|
|
|
switch ( identifier->leaftype ) {
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER_INT:
|
|
|
|
|
entered->valuetype = AKBASIC_TYPE_INTEGER;
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
PASS(errctx, aksl_atoll(buffer, &converted));
|
|
|
|
|
entered->intval = (int64_t)converted;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
break;
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER_FLOAT:
|
|
|
|
|
entered->valuetype = AKBASIC_TYPE_FLOAT;
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
PASS(errctx, aksl_atof(buffer, &entered->floatval));
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
entered->valuetype = AKBASIC_TYPE_STRING;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strlen(buffer) < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE,
|
|
|
|
|
"Input line exceeds the %d character limit", AKBASIC_MAX_STRING_LENGTH - 1);
|
|
|
|
|
strncpy(entered->stringval, buffer, AKBASIC_MAX_STRING_LENGTH - 1);
|
|
|
|
|
entered->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_environment_assign(obj->environment, identifier, entered, &unused));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* LIST and DELETE share the range grammar: bare, n, -n, or n-n. */
|
|
|
|
|
static akerr_ErrorContext *parse_line_range(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, int64_t *startidx, int64_t *endidx)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
|
|
|
|
|
*startidx = 0;
|
|
|
|
|
*endidx = AKBASIC_MAX_SOURCE_LINES - 1;
|
|
|
|
|
if ( expr == NULL || expr->right == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( expr->right->leaftype == AKBASIC_LEAF_BINARY &&
|
|
|
|
|
expr->right->operator_ == AKBASIC_TOK_MINUS ) {
|
|
|
|
|
/* n-n: a subtraction leaf is how the expression parser sees a range. */
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right->left, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected a line number range");
|
|
|
|
|
*startidx = value->intval;
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right->right, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected a line number range");
|
|
|
|
|
*endidx = value->intval;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
if ( expr->right->leaftype == AKBASIC_LEAF_UNARY &&
|
|
|
|
|
expr->right->operator_ == AKBASIC_TOK_MINUS ) {
|
2026-07-31 11:35:14 -04:00
|
|
|
/* -n: from the start through n. A unary leaf's operand is on .left. */
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right->left, &value));
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected a line number range");
|
|
|
|
|
*endidx = value->intval;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
/* n: from n to the end. */
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected a line number range");
|
|
|
|
|
*startidx = value->intval;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char line[AKBASIC_MAX_LINE_LENGTH * 2];
|
|
|
|
|
int64_t startidx = 0;
|
|
|
|
|
int64_t endidx = 0;
|
|
|
|
|
int64_t i = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx));
|
|
|
|
|
if ( startidx < 0 ) {
|
|
|
|
|
startidx = 0;
|
|
|
|
|
}
|
|
|
|
|
if ( endidx >= AKBASIC_MAX_SOURCE_LINES ) {
|
|
|
|
|
endidx = AKBASIC_MAX_SOURCE_LINES - 1;
|
|
|
|
|
}
|
|
|
|
|
for ( i = startidx; i <= endidx; i++ ) {
|
|
|
|
|
if ( obj->source[i].code[0] == '\0' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
snprintf(line, sizeof(line), "%" PRId64 " %s", i, obj->source[i].code);
|
|
|
|
|
PASS(errctx, akbasic_runtime_println(obj, line));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_delete(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int64_t startidx = 0;
|
|
|
|
|
int64_t endidx = 0;
|
|
|
|
|
int64_t i = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx));
|
|
|
|
|
if ( startidx < 0 ) {
|
|
|
|
|
startidx = 0;
|
|
|
|
|
}
|
|
|
|
|
if ( endidx >= AKBASIC_MAX_SOURCE_LINES ) {
|
|
|
|
|
endidx = AKBASIC_MAX_SOURCE_LINES - 1;
|
|
|
|
|
}
|
|
|
|
|
for ( i = startidx; i <= endidx; i++ ) {
|
|
|
|
|
obj->source[i].code[0] = '\0';
|
|
|
|
|
obj->source[i].lineno = 0;
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Resolve a DLOAD/DSAVE filename argument to a string. */
|
|
|
|
|
static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, char *dest, size_t len)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected a filename");
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
|
|
|
"Expected a filename string");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (value->stringval[0] != '\0'), AKBASIC_ERR_VALUE,
|
|
|
|
|
"Filename must not be empty");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strlen(value->stringval) < len), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"Filename exceeds the %zu character limit", len - 1);
|
|
|
|
|
strncpy(dest, value->stringval, len - 1);
|
|
|
|
|
dest[len - 1] = '\0';
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char filename[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
|
char buffer[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
char scanned[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
FILE *fp = NULL;
|
|
|
|
|
size_t used = 0;
|
|
|
|
|
int64_t i = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* aksl_fopen does not NULL-check pathname or mode and fopen(NULL, ...) is
|
|
|
|
|
* undefined, so the name is validated here before it is handed over --
|
|
|
|
|
* deps/libakstdlib/TODO.md 2.2.2.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename)));
|
|
|
|
|
|
|
|
|
|
/* DLOAD replaces the program in memory, so clear it before reading. */
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
|
|
|
|
obj->source[i].code[0] = '\0';
|
|
|
|
|
obj->source[i].lineno = 0;
|
|
|
|
|
}
|
|
|
|
|
obj->environment->lineno = 0;
|
|
|
|
|
obj->environment->nextline = 0;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, aksl_fopen(filename, "r", &fp));
|
|
|
|
|
while ( fgets(buffer, sizeof(buffer), fp) != NULL ) {
|
|
|
|
|
used = strlen(buffer);
|
|
|
|
|
while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) {
|
|
|
|
|
buffer[used - 1] = '\0';
|
|
|
|
|
used -= 1;
|
|
|
|
|
}
|
|
|
|
|
if ( buffer[0] == '\0' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* PASS inside the loop, never CATCH: CATCH expands to a break, which
|
|
|
|
|
* would leave this loop rather than the ATTEMPT and let the rest of
|
|
|
|
|
* the block run with an error pending.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
|
|
|
|
|
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
|
|
|
|
|
}
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
if ( fp != NULL ) {
|
|
|
|
|
IGNORE(aksl_fclose(fp));
|
|
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dsave(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char filename[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
|
char line[AKBASIC_MAX_LINE_LENGTH * 2];
|
|
|
|
|
FILE *fp = NULL;
|
|
|
|
|
int64_t i = 0;
|
|
|
|
|
int count = 0;
|
Consume libakgl 0.2.0 and libakstdlib 0.2.0
Rebasing the defect filing onto libakgl's origin/main brought two upstream
commits with it -- "Consume libakstdlib 0.2.0" and "Bump libakgl to 0.2.0" --
and libakgl's sources now use the 0.2.0 akstdlib API. Our CMakeLists declares
akstdlib::akstdlib from our own tree first, so libakgl was compiling 0.2.0-era
code against 0.1.0 headers and failing on aksl_fwrite, aksl_fread and
aksl_realpath. The two have to move together.
The code change is one call site. aksl_fwrite now takes a required size_t
*nmemb_out and reports a short transfer as AKERR_IO rather than a silent
success, so DSAVE onto a full disk is an error a program sees where before it
reported nothing. The count is passed and discarded on purpose: the library does
the noticing now.
The documentation change is larger, because libakstdlib 0.2.0 fixed all six of
the confirmed defects TODO.md section 1.9 was built around. That section was a
table of bans; leaving it would send an agent around a workaround for functions
that now work. The aksl_ato* family raises AKERR_VALUE on no digits or trailing
junk and ERANGE on overflow -- exactly the contract that section demanded --
aksl_list_append no longer truncates, aksl_list_iterate no longer skips the
first half, AKERR_ITERATOR_BREAK stops a tree traversal, and aksl_realpath no
longer reads uninitialised memory. One caveat survives: aksl_strhash_djb2 still
sign-extends char, which section 1.3 already covers and the symbol tables still
cannot reach.
src/convert.c has therefore outlived its reason, and is deliberately left in
place. Its own note said to delete it when libakstdlib grew the contract, and
that condition is now met -- but doing it touches four call sites, changes the
raised status from AKBASIC_ERR_VALUE to AKERR_VALUE where section 1.8 says
message text is part of the acceptance contract, and would silently gut the CI
mutation job, which is bounded to src/convert.c and src/symtab.c. Section 1.9
now lists all of that. Worth doing on purpose rather than as a side effect of a
version bump.
libakgl defect #14 is closed by its own 1066ac7, which bumped it to 0.2.0 while
this was being written. The requirement is no longer pinned by submodule commit
in the README, because AKGL_VERSION_AT_LEAST(0, 2, 0) finally means something.
70/70 core, 71/71 with libakgl, 70/70 under ASan+UBSan, clean under
-Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:53:41 -04:00
|
|
|
size_t written = 0;
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename)));
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, aksl_fopen(filename, "w", &fp));
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
|
|
|
|
if ( obj->source[i].code[0] == '\0' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
snprintf(line, sizeof(line), "%" PRId64 " %s\n", i, obj->source[i].code);
|
Consume libakgl 0.2.0 and libakstdlib 0.2.0
Rebasing the defect filing onto libakgl's origin/main brought two upstream
commits with it -- "Consume libakstdlib 0.2.0" and "Bump libakgl to 0.2.0" --
and libakgl's sources now use the 0.2.0 akstdlib API. Our CMakeLists declares
akstdlib::akstdlib from our own tree first, so libakgl was compiling 0.2.0-era
code against 0.1.0 headers and failing on aksl_fwrite, aksl_fread and
aksl_realpath. The two have to move together.
The code change is one call site. aksl_fwrite now takes a required size_t
*nmemb_out and reports a short transfer as AKERR_IO rather than a silent
success, so DSAVE onto a full disk is an error a program sees where before it
reported nothing. The count is passed and discarded on purpose: the library does
the noticing now.
The documentation change is larger, because libakstdlib 0.2.0 fixed all six of
the confirmed defects TODO.md section 1.9 was built around. That section was a
table of bans; leaving it would send an agent around a workaround for functions
that now work. The aksl_ato* family raises AKERR_VALUE on no digits or trailing
junk and ERANGE on overflow -- exactly the contract that section demanded --
aksl_list_append no longer truncates, aksl_list_iterate no longer skips the
first half, AKERR_ITERATOR_BREAK stops a tree traversal, and aksl_realpath no
longer reads uninitialised memory. One caveat survives: aksl_strhash_djb2 still
sign-extends char, which section 1.3 already covers and the symbol tables still
cannot reach.
src/convert.c has therefore outlived its reason, and is deliberately left in
place. Its own note said to delete it when libakstdlib grew the contract, and
that condition is now met -- but doing it touches four call sites, changes the
raised status from AKBASIC_ERR_VALUE to AKERR_VALUE where section 1.8 says
message text is part of the acceptance contract, and would silently gut the CI
mutation job, which is bounded to src/convert.c and src/symtab.c. Section 1.9
now lists all of that. Worth doing on purpose rather than as a side effect of a
version bump.
libakgl defect #14 is closed by its own 1066ac7, which bumped it to 0.2.0 while
this was being written. The requirement is no longer pinned by submodule commit
in the README, because AKGL_VERSION_AT_LEAST(0, 2, 0) finally means something.
70/70 core, 71/71 with libakgl, 70/70 under ASan+UBSan, clean under
-Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:53:41 -04:00
|
|
|
/*
|
|
|
|
|
* The written count is required by libakstdlib 0.2.0 and discarded
|
|
|
|
|
* here on purpose: a short write is no longer something a caller has
|
|
|
|
|
* to notice for itself, because that release made it an AKERR_IO
|
|
|
|
|
* rather than a silent success. Before it, a DSAVE onto a full disk
|
|
|
|
|
* reported nothing.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, aksl_fwrite(line, 1, strlen(line), fp, &written));
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
if ( fp != NULL ) {
|
|
|
|
|
IGNORE(aksl_fclose(fp));
|
|
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_if(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
|
|
|
|
/*
|
|
|
|
|
* Unreachable in practice: the IF parse handler produces a BRANCH leaf, and
|
|
|
|
|
* evaluate() handles BRANCH directly. It exists so the table has an exec
|
|
|
|
|
* handler for IF and a stray IF leaf produces a diagnosis rather than
|
|
|
|
|
* "Unknown command".
|
|
|
|
|
*/
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_STATE, "Malformed IF statement");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The loop condition, evaluated at the bottom of the structure. A negative step
|
|
|
|
|
* means the loop runs while the counter is at or above the TO value; a positive
|
|
|
|
|
* one, at or below. True means the loop is finished.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *evaluate_for_condition(akbasic_Runtime *obj, akbasic_Value *counter, bool *met)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value zero;
|
|
|
|
|
akbasic_Value scratch;
|
|
|
|
|
akbasic_Value *truth = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (counter != NULL), AKERR_NULLPOINTER, "NIL pointer for rval");
|
|
|
|
|
PASS(errctx, akbasic_value_zero(&zero));
|
|
|
|
|
zero.valuetype = AKBASIC_TYPE_INTEGER;
|
|
|
|
|
zero.intval = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_value_less_than(&obj->environment->forStepValue, &zero, &scratch, &truth));
|
|
|
|
|
if ( akbasic_value_is_true(truth) ) {
|
|
|
|
|
PASS(errctx, akbasic_value_greater_than_equal(&obj->environment->forToValue, counter,
|
|
|
|
|
&scratch, &truth));
|
|
|
|
|
} else {
|
|
|
|
|
PASS(errctx, akbasic_value_less_than_equal(&obj->environment->forToValue, counter,
|
|
|
|
|
&scratch, &truth));
|
|
|
|
|
}
|
|
|
|
|
*met = akbasic_value_is_true(truth);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_for(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *assignval = NULL;
|
|
|
|
|
akbasic_Value *tmp = NULL;
|
|
|
|
|
akbasic_Value *counter = NULL;
|
|
|
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
|
bool met = false;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
|
|
|
|
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(expr->right->left != NULL &&
|
|
|
|
|
(expr->right->left->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
|
|
|
|
expr->right->left->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT ||
|
|
|
|
|
expr->right->left->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING)),
|
|
|
|
|
AKBASIC_ERR_SYNTAX, "Expected variable in FOR loop");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &assignval));
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->left->identifier,
|
|
|
|
|
&obj->environment->forNextVariable));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"Unable to get loop variable %s", expr->right->left->identifier);
|
|
|
|
|
PASS(errctx, akbasic_variable_set_subscript(obj->environment->forNextVariable, assignval,
|
|
|
|
|
zerosubscript, 1));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, obj->environment->forToLeaf, &tmp));
|
|
|
|
|
PASS(errctx, akbasic_value_clone(tmp, &obj->environment->forToValue));
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, obj->environment->forStepLeaf, &tmp));
|
|
|
|
|
PASS(errctx, akbasic_value_clone(tmp, &obj->environment->forStepValue));
|
|
|
|
|
obj->environment->forToLeaf = NULL;
|
|
|
|
|
obj->environment->forStepLeaf = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_variable_get_subscript(obj->environment->forNextVariable,
|
|
|
|
|
zerosubscript, 1, &counter));
|
|
|
|
|
PASS(errctx, evaluate_for_condition(obj, counter, &met));
|
|
|
|
|
if ( met ) {
|
|
|
|
|
/* Zero iterations: skip the body entirely by waiting for the NEXT. */
|
|
|
|
|
PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "NEXT"));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *nextvar = NULL;
|
|
|
|
|
akbasic_Value *counter = NULL;
|
|
|
|
|
akbasic_Value *updated = NULL;
|
|
|
|
|
akbasic_Value scratch;
|
|
|
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
|
bool met = false;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->forNextVariable != NULL), AKBASIC_ERR_STATE,
|
|
|
|
|
"NEXT outside the context of FOR");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Expected NEXT IDENTIFIER");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
|
|
|
|
expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
|
|
|
|
|
AKBASIC_ERR_TYPE, "FOR ... NEXT only valid over INT and FLOAT types");
|
|
|
|
|
|
|
|
|
|
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A NEXT for someone else's loop variable: this environment is done, hand
|
|
|
|
|
* the line back to the parent and pop. That is how nested loops unwind.
|
|
|
|
|
*/
|
|
|
|
|
if ( strcmp(expr->right->identifier, obj->environment->forNextVariable->name) != 0 ) {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
|
|
|
"NEXT in an orphaned environment");
|
|
|
|
|
obj->environment->parent->nextline = obj->environment->nextline;
|
|
|
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
|
|
|
*dest = &obj->staticFalseValue;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &nextvar));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"Unable to get loop variable %s", expr->right->identifier);
|
|
|
|
|
PASS(errctx, akbasic_variable_get_subscript(nextvar, zerosubscript, 1, &counter));
|
|
|
|
|
PASS(errctx, evaluate_for_condition(obj, counter, &met));
|
|
|
|
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
|
|
|
|
|
|
|
|
|
if ( met ) {
|
|
|
|
|
if ( obj->environment->parent != NULL ) {
|
|
|
|
|
obj->environment->parent->nextline = obj->environment->nextline;
|
|
|
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Advance the counter. The stored value is mutable, so math_plus updates it
|
|
|
|
|
* in place -- see TODO.md section 12 item 4. Changing that without changing
|
|
|
|
|
* this breaks every FOR loop.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_value_math_plus(counter, &obj->environment->forStepValue,
|
|
|
|
|
&scratch, &updated));
|
|
|
|
|
obj->environment->nextline = obj->environment->loopFirstLine;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_exit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx,
|
|
|
|
|
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED),
|
|
|
|
|
AKBASIC_ERR_STATE, "EXIT outside the context of FOR");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
|
|
|
"EXIT in an orphaned environment");
|
|
|
|
|
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
|
|
|
|
/*
|
|
|
|
|
* The reference pops without clearing the wait, which leaves the parent
|
|
|
|
|
* waiting for a NEXT that will never arrive (TODO.md section 12 item 8). The
|
|
|
|
|
* wait is cleared here first: leaving it set would hang the interpreter
|
|
|
|
|
* rather than merely misbehave, and no golden case depends on the hang.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
|
|
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_read(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* READ does not read: it declares that the next DATA line should fill these
|
|
|
|
|
* identifiers, and skips forward until one appears.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "DATA"));
|
|
|
|
|
obj->environment->readIdentifierIdx = 0;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_data(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Environment *env = obj->environment;
|
|
|
|
|
akbasic_ASTLeaf *literal = NULL;
|
|
|
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
|
|
|
akbasic_ASTLeaf assign;
|
|
|
|
|
akbasic_Value *unused = NULL;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NIL expression or argument list");
|
|
|
|
|
|
|
|
|
|
for ( literal = expr->right->right; literal != NULL; literal = literal->right ) {
|
|
|
|
|
if ( env->readIdentifierIdx >= AKBASIC_MAX_LEAVES ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
identifier = env->readIdentifierLeaves[env->readIdentifierIdx];
|
|
|
|
|
if ( identifier == NULL ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Build the assignment by hand rather than through the parser: the
|
|
|
|
|
* identifier leaf is a stored copy and the literal belongs to this
|
|
|
|
|
* line's pool, so there is no source text to re-parse.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_leaf_init(&assign, AKBASIC_LEAF_BINARY));
|
|
|
|
|
assign.left = identifier;
|
|
|
|
|
assign.right = literal;
|
|
|
|
|
assign.operator_ = AKBASIC_TOK_ASSIGNMENT;
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, &assign, &unused));
|
|
|
|
|
env->readIdentifierIdx += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( literal == NULL &&
|
|
|
|
|
env->readIdentifierIdx < AKBASIC_MAX_LEAVES &&
|
|
|
|
|
env->readIdentifierLeaves[env->readIdentifierIdx] != NULL ) {
|
|
|
|
|
/* Out of DATA with READ items outstanding: stay in waiting mode. */
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_stop_waiting(env, "DATA"));
|
|
|
|
|
env->lineno = env->readReturnLine;
|
|
|
|
|
env->readIdentifierIdx = 0;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|