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>
This commit is contained in:
275
src/runtime_structure.c
Normal file
275
src/runtime_structure.c
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* @file runtime_structure.c
|
||||
* @brief The group A verbs: DO, LOOP, BEGIN, BEND, ON and END.
|
||||
*
|
||||
* Block structure, built on the same `waitingForCommand` machinery `FOR`/`NEXT`
|
||||
* uses (section 1.6): a scope records the verb it is skipping forward to, and
|
||||
* nothing executes until that verb turns up. Everything here is a variation on
|
||||
* that one idea.
|
||||
*
|
||||
* **The line-based limitation applies to all of it.** Skipping works a source
|
||||
* line at a time, so a whole loop written on one line -- `DO : PRINT 1 : LOOP` --
|
||||
* does not loop, exactly as `FOR I=1 TO 3 : PRINT I : NEXT I` does not. That is
|
||||
* recorded in TODO.md section 4 and it wants its own piece of work: making the
|
||||
* skip operate on statements rather than lines.
|
||||
*
|
||||
* None of these verbs is in the Go reference, which lists all of them as
|
||||
* unimplemented, so the semantics come from Commodore BASIC 7.0.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/args.h>
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.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 )
|
||||
|
||||
/**
|
||||
* @brief Should a loop carrying this condition keep going?
|
||||
*
|
||||
* `WHILE` continues while the condition holds and `UNTIL` continues until it
|
||||
* does, which is the same test read two ways. A loop with no condition on that
|
||||
* end always continues -- `DO ... LOOP` is an infinite loop, and `EXIT` or a
|
||||
* `GOTO` is how a program leaves it.
|
||||
*/
|
||||
static akerr_ErrorContext *loop_continues(akbasic_Runtime *obj, akbasic_ASTLeaf *condition, int kind, bool *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Value *value = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in loop_continues");
|
||||
if ( kind == AKBASIC_LOOPCOND_NONE || condition == NULL ) {
|
||||
*dest = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, condition, &value));
|
||||
*dest = akbasic_value_is_truthy(value);
|
||||
if ( kind == AKBASIC_LOOPCOND_UNTIL ) {
|
||||
*dest = !(*dest);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- DO / LOOP -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_do(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool enter = false;
|
||||
|
||||
(void)expr; (void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in DO");
|
||||
/*
|
||||
* The parse handler has already pushed the scope and stored the condition,
|
||||
* the way FOR's does -- so by the time this runs, `obj->environment` is the
|
||||
* loop's own.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
||||
"DO did not establish its own scope");
|
||||
|
||||
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
||||
obj->environment->doConditionKind, &enter));
|
||||
if ( !enter ) {
|
||||
/*
|
||||
* `DO WHILE` with a condition that is already false runs no body at all,
|
||||
* so skip forward to the LOOP rather than executing the lines between.
|
||||
* Same mechanism a zero-iteration FOR uses.
|
||||
*/
|
||||
PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "LOOP"));
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Value *value = NULL;
|
||||
akbasic_ASTLeaf *arg = NULL;
|
||||
bool again = false;
|
||||
int kind = AKBASIC_LOOPCOND_NONE;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in LOOP");
|
||||
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
||||
"LOOP outside the context of DO");
|
||||
|
||||
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
||||
|
||||
/* An EXIT sent us here; the loop is over whatever either condition says. */
|
||||
if ( obj->environment->exiting ) {
|
||||
obj->environment->exiting = false;
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||
again = false;
|
||||
} else {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||
/*
|
||||
* LOOP's own condition, parsed here rather than by a parse handler: the
|
||||
* leaf is on this line and this line is still scanned, so unlike DO's
|
||||
* there is nothing to preserve across iterations.
|
||||
*/
|
||||
arg = (expr != NULL ? expr->right : NULL);
|
||||
if ( arg != NULL ) {
|
||||
kind = (int)arg->literal_int;
|
||||
PASS(errctx, loop_continues(obj, arg->left, kind, &again));
|
||||
} else {
|
||||
/*
|
||||
* A bare LOOP re-tests whatever DO carried. `DO WHILE c ... LOOP`
|
||||
* has to check `c` again at the bottom or the loop never ends.
|
||||
*/
|
||||
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
||||
obj->environment->doConditionKind, &again));
|
||||
}
|
||||
}
|
||||
(void)value;
|
||||
|
||||
if ( again ) {
|
||||
obj->environment->nextline = obj->environment->loopFirstLine;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"LOOP in an orphaned environment");
|
||||
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- BEGIN / BEND -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_begin(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_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in BEGIN");
|
||||
/*
|
||||
* Nothing to do when the block is entered. `IF c THEN BEGIN` reaches this
|
||||
* only when `c` was true, and the lines that follow are then ordinary lines
|
||||
* up to the BEND. The *false* case never gets here at all: the branch arms a
|
||||
* skip to BEND instead -- see the BRANCH case in akbasic_runtime_evaluate().
|
||||
*/
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_bend(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_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in BEND");
|
||||
/*
|
||||
* Either this is the end of a block that ran, in which case there is nothing
|
||||
* to do, or it is the BEND a skipped block was skipping to, in which case
|
||||
* stopping the skip is the whole job.
|
||||
*/
|
||||
if ( akbasic_environment_is_waiting_for(obj->environment, "BEND") ) {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "BEND"));
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- ON -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_on(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_ASTLeaf *arg = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
int64_t selector = 0;
|
||||
int64_t target = 0;
|
||||
int64_t returnline = 0;
|
||||
int index = 0;
|
||||
bool gosub = false;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in ON");
|
||||
|
||||
arg = akbasic_leaf_first_argument(expr);
|
||||
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
||||
"Expected ON (expression) GOTO|GOSUB (line) [, ...]");
|
||||
/* The parse handler puts the GOSUB flag in the first argument's literal. */
|
||||
gosub = (arg->literal_int != 0);
|
||||
arg = arg->next;
|
||||
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
||||
"Expected ON (expression) GOTO|GOSUB (line) [, ...]");
|
||||
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||||
"ON expected a number");
|
||||
selector = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
||||
? (int64_t)value->floatval : value->intval;
|
||||
|
||||
/*
|
||||
* One-based, and out of range is not an error: BASIC 7.0 falls through to
|
||||
* the next statement when the selector names no target, which is what makes
|
||||
* `ON X GOTO 100, 200` usable without a bounds check in the program.
|
||||
*/
|
||||
for ( arg = arg->next, index = 1; arg != NULL; arg = arg->next, index++ ) {
|
||||
if ( (int64_t)index != selector ) {
|
||||
continue;
|
||||
}
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER),
|
||||
AKBASIC_ERR_TYPE, "ON expected a line number or a label");
|
||||
target = value->intval;
|
||||
|
||||
if ( gosub ) {
|
||||
returnline = obj->environment->lineno + 1;
|
||||
PASS(errctx, akbasic_runtime_new_environment(obj));
|
||||
obj->environment->gosubReturnLine = returnline;
|
||||
obj->environment->nextline = target;
|
||||
} else {
|
||||
obj->environment->nextline = target;
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- END -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_end(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_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in END");
|
||||
/*
|
||||
* The program is over, which is not the same as the interpreter being over.
|
||||
* A file run ends; a REPL session goes back to its prompt. That is what
|
||||
* run_finished_mode already means, and it is the difference between END and
|
||||
* QUIT -- QUIT ends the interpreter whatever started it.
|
||||
*
|
||||
* Unlike STOP this does not arm CONT. A C128 allows CONT after END, but END
|
||||
* says the program finished and CONT after a *finished* program resumes into
|
||||
* whatever line happens to follow, which is a worse answer than refusing.
|
||||
*/
|
||||
PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user