Files
akbasic/src/runtime_structure.c
Ishikawa a29c7f34fe Fix generator teardown leaks, add RETURN-in-GEN and LOOP conditions on DO EACH
Review findings and follow-ups from PR #61 review:

- runtime_generator.c: akbasic_runtime_release_generator() now releases the
  forGeneratorEnv of every scope it walks through. Abandoning a generator
  that was itself suspended inside a FOR EACH over another generator
  stranded the inner generator's pool slot; a loop doing so exhausted the
  twelve-slot pool and died far from the cause.
- runtime.c/runtime.h: new akbasic_runtime_unwind_to_environment(), the
  shared teardown for the error unwinds in pump_generator() and
  call_function() -- both previously bare prev_environment() loops with the
  same suspended-generator blindness.
- runtime_commands.c: bare RETURN standing in a GEN's own frame ends the
  generator exactly as END GEN does -- a GEN is a function at heart. RETURN
  with a value there is refused (values leave a GEN only through EMIT). The
  no-frame error message now says "GOSUB, DEF, or GEN".
- runtime_structure.c: LOOP WHILE/UNTIL composes with DO EACH -- checked
  after each trip with the loop variable still holding that trip's value; a
  condition that stops the loop abandons the generator exactly as EXIT
  does. Previously the condition was silently ignored, while the verb
  reference documented it as working.
- parser_commands.c: trailing tokens after the generator call on a FOR
  EACH/DO EACH line are refused at parse. Previously they sat unparsed and
  blew up only after the loop completed, when the parent scope resumed the
  line mid-statement -- an error at the loop's end pointing at its start.
- tests/generators.c: pool-exhaustion tests for the nested-abandonment and
  LOOP-condition paths, RETURN semantics tests, and a direct test of the
  unwind primitive. Three new golden pairs cover RETURN, LOOP conditions
  and the misplaced-condition parse error.
- docs: RETURN and LOOP-condition semantics in 04-control-flow.md and
  11-verb-reference.md; corrected the self-recursion analogy (functions
  are re-entrant here). TODO.md 1.10 records the generator design
  decisions the code comments were already citing, plus the zero-arg
  parameter-list limitation. MAINTENANCE.md gains the abandoned-generators
  invariant those comments also cited.

Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:55:59 -04:00

333 lines
13 KiB
C

/**
* @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");
if ( obj->environment->isEachLoop ) {
akbasic_Environment *loopenv = obj->environment;
akbasic_ASTLeaf *var = (expr != NULL ? expr->right : NULL);
FAIL_ZERO_RETURN(errctx, (var != NULL && akbasic_leaf_is_identifier(var)),
AKBASIC_ERR_SYNTAX, "Expected DO EACH (variable) IN (generator call)");
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
"Expected DO EACH (variable) IN (generator call)");
PASS(errctx, akbasic_environment_get(loopenv, var->identifier, &loopenv->forNextVariable));
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
"Unable to get loop variable %s", var->identifier);
PASS(errctx, akbasic_runtime_generator_invoke(obj, loopenv, loopenv->forToLeaf));
loopenv->forToLeaf = NULL;
if ( loopenv->forGeneratorEnv == NULL ) {
/* The generator produced nothing: skip the body, same as DO WHILE
false does. */
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "LOOP"));
}
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
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"));
/* A live generator abandoned mid-run: release it too. */
if ( obj->environment->forGeneratorEnv != NULL ) {
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
obj->environment->forGeneratorEnv = NULL;
}
again = false;
} else if ( obj->environment->isEachLoop ) {
akbasic_Environment *loopenv = obj->environment;
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
/*
* A condition on the LOOP composes with EACH: it is checked after each
* trip through the body, with the loop variable still holding that
* trip's value, before the generator is pumped for the next one. A
* condition that says stop abandons the generator exactly as EXIT does.
*/
again = true;
arg = (expr != NULL ? expr->right : NULL);
if ( arg != NULL ) {
kind = (int)arg->literal_int;
PASS(errctx, loop_continues(obj, arg->left, kind, &again));
}
if ( !again && loopenv->forGeneratorEnv != NULL ) {
PASS(errctx, akbasic_runtime_release_generator(obj, loopenv->forGeneratorEnv));
loopenv->forGeneratorEnv = NULL;
}
if ( again && loopenv->forGeneratorEnv != NULL ) {
obj->environment = loopenv->forGeneratorEnv;
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
}
again = (again && loopenv->forGeneratorEnv != NULL);
} 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);
bool waiting = false;
(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.
*/
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "BEND", &waiting));
if ( waiting ) {
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);
}