All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m26s
akbasic CI Build / coverage (push) Successful in 4m0s
akbasic CI Build / sanitizers (push) Successful in 6m33s
akbasic CI Build / akgl_build (push) Successful in 9m32s
akbasic CI Build / mutation_test (push) Successful in 18m47s
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>
308 lines
13 KiB
C
308 lines
13 KiB
C
/**
|
|
* @file runtime_generator.c
|
|
* @brief GEN, EMIT and END GEN, plus the machinery FOR EACH/DO EACH share.
|
|
*
|
|
* A GEN is a DEF that yields more than once. Its body is skipped on the
|
|
* definitional pass exactly as a multi-line DEF's is -- `akbasic_parse_gen()`
|
|
* arms `akbasic_environment_wait_for_command(env, "END GEN")` the same way
|
|
* `akbasic_parse_def()` arms one for `RETURN` -- and it only ever really runs
|
|
* when a `FOR EACH`/`DO EACH` invokes it.
|
|
*
|
|
* That invocation pushes one environment for the whole lifetime of the loop,
|
|
* exactly as a GOSUB or a function call does, except that `EMIT` does not pop
|
|
* it: it hands control back to the loop without releasing anything, so the
|
|
* environment EMIT actually ran in -- which may be nested several levels
|
|
* below the GEN's own call frame, inside a FOR/DO/GOSUB the body wrote --
|
|
* still says exactly where to resume when `NEXT`/`LOOP` calls back into
|
|
* akbasic_runtime_pump_generator(). Only `END GEN` reached for real --
|
|
* meaning `obj->environment->isGenerator` is true and nothing is skipping
|
|
* forward to it -- actually releases the call frame, the way `RETURN`
|
|
* releases a DEF's call environment.
|
|
*/
|
|
|
|
#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_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in pump_generator");
|
|
/*
|
|
* The same per-line prologue akbasic_runtime_step() and
|
|
* akbasic_runtime_call_function() run, driving process_line_run() directly
|
|
* rather than through the ordinary step loop: a generator body is not the
|
|
* top-level program and nothing else is going to advance it.
|
|
*/
|
|
ATTEMPT {
|
|
while ( obj->environment != loopenv && obj->mode == AKBASIC_MODE_RUN ) {
|
|
CATCH(errctx, akbasic_runtime_zero(obj));
|
|
CATCH(errctx, akbasic_scanner_zero(obj));
|
|
CATCH(errctx, akbasic_runtime_process_line_run(obj));
|
|
}
|
|
} CLEANUP {
|
|
/*
|
|
* CLEANUP runs unconditionally -- it is not a `catch` -- so it is
|
|
* guarded on the one thing that tells success and failure apart here:
|
|
* whether `obj->environment` is still `loopenv`. On the ordinary
|
|
* success path it already is (that is the ATTEMPT loop's own exit
|
|
* condition), so this is a no-op there, exactly as it is meant to be.
|
|
* Only a genuine C-level failure -- the scanner or parser raised, or a
|
|
* runtime error escaped the swallow process_line_run() ordinarily does
|
|
* for a BASIC-level one -- leaves scopes active between here and
|
|
* loopenv, and only then does this force them back, taking
|
|
* `forGeneratorEnv` down with them since whatever it pointed at is
|
|
* among the scopes just released.
|
|
*/
|
|
if ( obj->environment != loopenv ) {
|
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, loopenv));
|
|
loopenv->forGeneratorEnv = NULL;
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Release a live generator, however deep EMIT left it suspended.
|
|
*
|
|
* `loopenv->forGeneratorEnv` is the *resume point*, not necessarily the GEN's
|
|
* own call frame -- EMIT may have run several levels down, inside a FOR/DO/
|
|
* GOSUB the body wrote around it. Abandoning it (EXIT) has to give back every
|
|
* environment from there up through the call frame itself, or everything
|
|
* above the resume point leaks.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param env The suspended resume point; walks up through its own parents.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
*/
|
|
akerr_ErrorContext *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Environment *walk = env;
|
|
akbasic_Environment *next = NULL;
|
|
bool isgen = false;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in release_generator");
|
|
while ( walk != NULL ) {
|
|
isgen = walk->isGenerator;
|
|
next = walk->parent;
|
|
/*
|
|
* A scope between the resume point and the call frame may be an EACH
|
|
* loop with its *own* generator suspended off to the side. Releasing
|
|
* the loop scope without releasing that generator strands it in the
|
|
* pool -- the walk goes through parents and a suspended generator is a
|
|
* child. Guarded on `used` so a generator already released as part of
|
|
* some enclosing teardown is not released twice.
|
|
*/
|
|
if ( walk->forGeneratorEnv != NULL && walk->forGeneratorEnv->used ) {
|
|
PASS(errctx, akbasic_runtime_release_generator(obj, walk->forGeneratorEnv));
|
|
}
|
|
walk->forGeneratorEnv = NULL;
|
|
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
|
if ( isgen ) {
|
|
break;
|
|
}
|
|
walk = next;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Environment *callenv = NULL;
|
|
akbasic_Environment *walk = NULL;
|
|
akbasic_FunctionDef *fndef = NULL;
|
|
akbasic_ASTLeaf *fnarg = NULL;
|
|
akbasic_ASTLeaf *paramleaf = NULL;
|
|
akbasic_Value *argvals[AKBASIC_MAX_CALL_ARGUMENTS];
|
|
akbasic_Value *unused = NULL;
|
|
void *fnptr = NULL;
|
|
int nargs = 0;
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL && callexpr != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in generator_invoke");
|
|
FAIL_ZERO_RETURN(errctx, (callexpr->leaftype == AKBASIC_LEAF_FUNCTION), AKBASIC_ERR_SYNTAX,
|
|
"Expected a generator call after IN");
|
|
|
|
/*
|
|
* GEN and DEF share the functions table (TODO.md's namespace decision for
|
|
* this feature), so this is the same lookup akbasic_runtime_call_function()
|
|
* does. The parser already proved the name resolves and the arity matches
|
|
* when it parsed `callexpr` -- akbasic_parser_expression() would not have
|
|
* produced an AKBASIC_LEAF_FUNCTION leaf otherwise -- so a miss here would
|
|
* mean the function table changed out from under a leaf built against it,
|
|
* which is not a case this needs its own message for.
|
|
*/
|
|
PASS(errctx, akbasic_environment_get_function(loopenv, callexpr->identifier, &fnptr));
|
|
fndef = (akbasic_FunctionDef *)fnptr;
|
|
FAIL_ZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
|
"%s is a DEF, not a GEN -- FOR EACH/DO EACH needs a generator",
|
|
fndef->name);
|
|
|
|
/*
|
|
* Self-recursion: walk the *parent* chain, not the pool. An environment
|
|
* reachable only through some other loop's `forGeneratorEnv` is a sibling
|
|
* invocation sitting detached between its own iterations, not an ancestor
|
|
* of this call -- nothing points from here to it via `parent`, so it never
|
|
* matches and independent or nested FOR EACH/DO EACH over the same GEN
|
|
* (even the same GEN with different arguments) is unaffected.
|
|
*/
|
|
for ( walk = loopenv; walk != NULL; walk = walk->parent ) {
|
|
if ( walk->isGenerator && walk->generatorFn == (void *)fndef ) {
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_STATE,
|
|
"GEN %s cannot FOR EACH/DO EACH over itself from its own body",
|
|
fndef->name);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Evaluated in the caller's own scope, before anything is pushed -- the
|
|
* same reason akbasic_runtime_user_function() evaluates every argument
|
|
* before binding the first one: a later argument must not see an earlier
|
|
* one already sitting in the callee's scope.
|
|
*/
|
|
fnarg = akbasic_leaf_first_argument(callexpr);
|
|
for ( ; fnarg != NULL; fnarg = fnarg->next ) {
|
|
FAIL_ZERO_RETURN(errctx, (nargs < AKBASIC_MAX_CALL_ARGUMENTS), AKBASIC_ERR_BOUNDS,
|
|
"%s was called with more than %d arguments",
|
|
callexpr->identifier, AKBASIC_MAX_CALL_ARGUMENTS);
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, fnarg, &argvals[nargs]));
|
|
nargs += 1;
|
|
}
|
|
|
|
/*
|
|
* One environment for the whole lifetime of the loop, exactly as GOSUB and
|
|
* a function call take one from the pool -- and unlike either, this one
|
|
* survives past the verb that pushed it, held alive by `loopenv`'s own
|
|
* reference until NEXT/LOOP exhausts or EXIT abandons it.
|
|
*/
|
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
|
callenv = obj->environment;
|
|
callenv->isGenerator = true;
|
|
callenv->generatorFn = (void *)fndef;
|
|
callenv->nextline = fndef->lineno;
|
|
loopenv->forGeneratorEnv = callenv;
|
|
|
|
paramleaf = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
|
|
for ( i = 0; i < nargs && paramleaf != NULL; i++ ) {
|
|
PASS(errctx, akbasic_environment_assign(callenv, paramleaf, argvals[i], &unused));
|
|
paramleaf = paramleaf->next;
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_gen(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 generator, exactly as DEF's does. */
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_emit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Environment *genenv = NULL;
|
|
akbasic_Environment *loopenv = NULL;
|
|
akbasic_Value *value = NULL;
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected EMIT (expression)");
|
|
/*
|
|
* EMIT is not necessarily standing directly in the environment
|
|
* akbasic_runtime_generator_invoke() pushed: a GEN body is ordinary BASIC
|
|
* and may nest its own FOR, DO or GOSUB around an EMIT, each of which
|
|
* pushes an environment of its own -- exactly what the issue's own
|
|
* ROOMOBJECTS example does. Walk up to the nearest one that really is a
|
|
* GEN's own call frame.
|
|
*/
|
|
for ( genenv = obj->environment; genenv != NULL && !genenv->isGenerator; genenv = genenv->parent ) {
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, (genenv != NULL), AKBASIC_ERR_STATE,
|
|
"EMIT outside the context of a GEN body");
|
|
loopenv = genenv->parent;
|
|
FAIL_ZERO_RETURN(errctx, (loopenv != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
"EMIT from an orphaned environment");
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
|
/*
|
|
* Straight into the loop variable's storage, bypassing the arithmetic
|
|
* akbasic_environment_assign() and evaluate_for_condition() carry for a
|
|
* plain FOR: an EACH variable takes whatever type the GEN emits, string or
|
|
* structure element included, and there is no TO/STEP to compare it
|
|
* against.
|
|
*/
|
|
PASS(errctx, akbasic_variable_set_subscript(loopenv->forNextVariable, value, zerosubscript, 1));
|
|
loopenv->nextline = loopenv->loopFirstLine;
|
|
/*
|
|
* The resume point, which may be several levels below `genenv` -- whatever
|
|
* nested FOR/DO/GOSUB environment this EMIT actually ran in. NEXT/LOOP
|
|
* reactivates exactly this one, so the nested structure picks up exactly
|
|
* where it left off rather than restarting at the top of the GEN body.
|
|
*/
|
|
loopenv->forGeneratorEnv = obj->environment;
|
|
/*
|
|
* Not a pop, and not a single-level detach either: everything between here
|
|
* and `loopenv` -- `genenv` and any of its own descendants -- has to
|
|
* survive untouched to be resumed, so control moves to `loopenv` directly
|
|
* rather than walking the chain one release at a time.
|
|
*/
|
|
obj->environment = loopenv;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_end_gen(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;
|
|
/*
|
|
* A END GEN reached while skipping forward to one is the end of a GEN
|
|
* body's *definition*, not the end of a call -- the same distinction
|
|
* RETURN draws for DEF.
|
|
*/
|
|
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "END GEN", &waiting));
|
|
if ( waiting ) {
|
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "END GEN"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->isGenerator), AKBASIC_ERR_STATE,
|
|
"END GEN outside the context of a generator invocation");
|
|
/*
|
|
* Real exhaustion: release this environment and detach in the same
|
|
* motion prev_environment() always does, then clear the parent's
|
|
* reference to it so a caller pumping this loop can tell "still alive"
|
|
* apart from "nothing left to resume".
|
|
*/
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
obj->environment->forGeneratorEnv = NULL;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|