Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57)
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,10 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R
|
||||
obj->doConditionLeaf = NULL;
|
||||
obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
|
||||
obj->isDoLoop = false;
|
||||
obj->isGenerator = false;
|
||||
obj->generatorFn = NULL;
|
||||
obj->isEachLoop = false;
|
||||
obj->forGeneratorEnv = NULL;
|
||||
obj->gosubReturnLine = 0;
|
||||
obj->readReturnLine = 0;
|
||||
obj->readIdentifierIdx = 0;
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
#include "verbs.h"
|
||||
|
||||
/* Shared by akbasic_parse_for() and akbasic_parse_do(); defined after akbasic_parse_def(). */
|
||||
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr);
|
||||
|
||||
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -309,8 +312,45 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d
|
||||
akbasic_Environment *newenv = NULL;
|
||||
akbasic_ASTLeaf *expr = NULL;
|
||||
akbasic_ASTLeaf *condition = NULL;
|
||||
akbasic_Token *peeked = NULL;
|
||||
int kind = AKBASIC_LOOPCOND_NONE;
|
||||
int64_t firstline = parent->lineno + 1;
|
||||
int cmp = 0;
|
||||
|
||||
/*
|
||||
* DO EACH <variable> IN <generator call> ... LOOP. Mutually exclusive with
|
||||
* DO WHILE/UNTIL on the same DO, so this is checked first and returns
|
||||
* before any of the WHILE/UNTIL machinery runs.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||
if ( cmp == 0 ) {
|
||||
akbasic_ASTLeaf *var = NULL;
|
||||
akbasic_ASTLeaf *callexpr = NULL;
|
||||
|
||||
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||
|
||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||
newenv = runtime->environment;
|
||||
runtime->environment = parent;
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
newenv->isDoLoop = true;
|
||||
newenv->isEachLoop = true;
|
||||
newenv->loopFirstLine = firstline;
|
||||
/* See akbasic_parse_for()'s EACH branch for why this is not cloned. */
|
||||
newenv->forToLeaf = callexpr;
|
||||
|
||||
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_command(expr, "DO", var));
|
||||
|
||||
runtime->environment = newenv;
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||
newenv = runtime->environment;
|
||||
@@ -883,6 +923,134 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* GEN NAME(parameters) ... END GEN
|
||||
*
|
||||
* A DEF that yields more than once, in the same shape a multi-line DEF is:
|
||||
* the header is parsed, the parameter list is read with parse_def_parameters()
|
||||
* (GEN and DEF share the functions table -- TODO.md's namespace decision for
|
||||
* this feature -- so a name cannot be both), and the body is skipped on this,
|
||||
* the definitional pass, by arming akbasic_environment_wait_for_command() for
|
||||
* "END GEN" instead of "RETURN". It only really executes when a FOR EACH/DO
|
||||
* EACH invokes it through akbasic_runtime_generator_invoke(), which sets
|
||||
* `nextline` to `fndef->lineno` directly and never runs this line again.
|
||||
*
|
||||
* There is no single-expression form: a GEN with nothing to loop over is just
|
||||
* a DEF, and EMIT already needs a body to sit in.
|
||||
*/
|
||||
akerr_ErrorContext *akbasic_parse_gen(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Runtime *runtime = parser->runtime;
|
||||
akbasic_ASTLeaf *identifier = NULL;
|
||||
akbasic_ASTLeaf *arglist = NULL;
|
||||
akbasic_ASTLeaf *command = NULL;
|
||||
akbasic_FunctionDef *fndef = NULL;
|
||||
size_t namelen = 0;
|
||||
size_t i = 0;
|
||||
|
||||
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
||||
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
|
||||
AKBASIC_ERR_SYNTAX, "Expected identifier");
|
||||
|
||||
PASS(errctx, parse_def_parameters(parser, &arglist));
|
||||
|
||||
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
|
||||
|
||||
/* Uppercase the name: verbs, functions and generators are all case-insensitive. */
|
||||
PASS(errctx, aksl_strlen(identifier->identifier, &namelen));
|
||||
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)),
|
||||
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
|
||||
for ( i = 0; i < namelen; i++ ) {
|
||||
char c = identifier->identifier[i];
|
||||
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
|
||||
}
|
||||
fndef->name[namelen] = '\0';
|
||||
|
||||
fndef->expression = NULL;
|
||||
fndef->isGenerator = true;
|
||||
PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "END GEN"));
|
||||
PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist));
|
||||
fndef->lineno = runtime->environment->lineno + 1;
|
||||
|
||||
PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0));
|
||||
|
||||
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
||||
PASS(errctx, akbasic_leaf_new_command(command, "GEN", NULL));
|
||||
*dest = command;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* END [GEN]
|
||||
*
|
||||
* Bare END finishes the program, the default command path's job before this
|
||||
* handler existed. `END GEN` is a GEN body's own closing verb, and it is
|
||||
* never scanned as one token -- GEN follows END as an ordinary COMMAND token
|
||||
* on the same line -- so this is what tells the two apart and builds the
|
||||
* compound leaf akbasic_cmd_end_gen dispatches on, the same trick
|
||||
* akbasic_parse_print() uses for `PRINT #`.
|
||||
*/
|
||||
akerr_ErrorContext *akbasic_parse_end(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_ASTLeaf *expr = NULL;
|
||||
akbasic_ASTLeaf *right = NULL;
|
||||
akbasic_Token *peeked = NULL;
|
||||
int cmp = 0;
|
||||
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||
PASS(errctx, aksl_strcmp(peeked->lexeme, "GEN", &cmp));
|
||||
if ( cmp == 0 ) {
|
||||
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_command(expr, "END GEN", NULL));
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
|
||||
/* A plain END, matching what the default command path used to do. */
|
||||
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||
peeked->tokentype != AKBASIC_TOK_COLON ) {
|
||||
PASS(errctx, akbasic_parser_expression(parser, &right));
|
||||
}
|
||||
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_command(expr, "END", right));
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* EACH <variable> IN <generator call>
|
||||
*
|
||||
* Shared by akbasic_parse_for() and akbasic_parse_do(), both of which consume
|
||||
* the leading EACH themselves (it is what tells them this is an EACH loop
|
||||
* rather than their ordinary form) before calling this for the rest.
|
||||
*/
|
||||
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Token *word = NULL;
|
||||
int cmp = 0;
|
||||
|
||||
PASS(errctx, akbasic_parser_expression(parser, var));
|
||||
FAIL_ZERO_RETURN(errctx, (*var != NULL && akbasic_leaf_is_identifier(*var)), AKBASIC_ERR_SYNTAX,
|
||||
"Expected EACH (variable) IN (generator call)");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX,
|
||||
"Expected IN after EACH (variable)");
|
||||
PASS(errctx, akbasic_parser_previous(parser, &word));
|
||||
PASS(errctx, aksl_strcmp(word->lexeme, "IN", &cmp));
|
||||
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IN after EACH (variable)");
|
||||
|
||||
PASS(errctx, akbasic_parser_expression(parser, callexpr));
|
||||
FAIL_ZERO_RETURN(errctx, (*callexpr != NULL && (*callexpr)->leaftype == AKBASIC_LEAF_FUNCTION),
|
||||
AKBASIC_ERR_SYNTAX, "Expected a generator call after IN");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* FOR ... TO .... [STEP ...]
|
||||
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
||||
@@ -898,11 +1066,57 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
|
||||
akbasic_ASTLeaf *assignment = NULL;
|
||||
akbasic_ASTLeaf *expr = NULL;
|
||||
akbasic_Token *operator_ = NULL;
|
||||
akbasic_Token *peeked = NULL;
|
||||
akbasic_Environment *parent = runtime->environment;
|
||||
akbasic_Environment *newenv = NULL;
|
||||
int64_t firstline = 0;
|
||||
int cmp = 0;
|
||||
|
||||
/*
|
||||
* FOR EACH <variable> IN <generator call>. Checked before the leaf right of
|
||||
* FOR is required to be an assignment, because EACH is the one other thing
|
||||
* that leaf is allowed to be.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||
if ( cmp == 0 ) {
|
||||
akbasic_ASTLeaf *var = NULL;
|
||||
akbasic_ASTLeaf *callexpr = NULL;
|
||||
|
||||
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||
firstline = parent->lineno + 1;
|
||||
|
||||
/*
|
||||
* Pushed the same way a plain FOR's environment is: while the
|
||||
* parent is still active, so the call expression's identifiers
|
||||
* resolve in the caller's scope rather than the loop's own.
|
||||
*/
|
||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||
newenv = runtime->environment;
|
||||
runtime->environment = parent;
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
newenv->isEachLoop = true;
|
||||
newenv->loopFirstLine = firstline;
|
||||
/*
|
||||
* Stashed on forToLeaf rather than cloned into a leaf pool: unlike
|
||||
* DO's condition, this expression is evaluated exactly once, by
|
||||
* akbasic_cmd_for() on this same pass before the per-line leaf
|
||||
* storage it lives in is reused -- see akbasic_runtime_generator_invoke().
|
||||
*/
|
||||
newenv->forToLeaf = callexpr;
|
||||
|
||||
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_command(expr, "FOR", var));
|
||||
|
||||
runtime->environment = newenv;
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
||||
AKBASIC_ERR_SYNTAX,
|
||||
|
||||
@@ -143,17 +143,24 @@ akerr_ErrorContext *akbasic_runtime_new_environment(akbasic_Runtime *obj)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||
akerr_ErrorContext *akbasic_runtime_detach_environment(akbasic_Runtime *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Environment *popped = NULL;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in detach_environment");
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"No previous environment to return to");
|
||||
popped = obj->environment;
|
||||
obj->environment = popped->parent;
|
||||
obj->environment = obj->environment->parent;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in release_environment");
|
||||
|
||||
/*
|
||||
* Give back the variables this scope created, as well as the scope.
|
||||
@@ -173,9 +180,9 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
||||
* reached" on a four-line program.
|
||||
*/
|
||||
for ( i = 0; i < popped->variables.capacity; i++ ) {
|
||||
akbasic_Variable *variable = (akbasic_Variable *)popped->variables.slots[i].value;
|
||||
if ( popped->variables.slots[i].used && variable != NULL ) {
|
||||
for ( i = 0; i < env->variables.capacity; i++ ) {
|
||||
akbasic_Variable *variable = (akbasic_Variable *)env->variables.slots[i].value;
|
||||
if ( env->variables.slots[i].used && variable != NULL ) {
|
||||
variable->used = false;
|
||||
}
|
||||
}
|
||||
@@ -185,7 +192,19 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||
* here the pool is finite, so an unreleased environment is a bug that shows
|
||||
* up as exhaustion a few thousand GOSUBs later.
|
||||
*/
|
||||
popped->used = false;
|
||||
env->used = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Environment *popped = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
||||
popped = obj->environment;
|
||||
PASS(errctx, akbasic_runtime_detach_environment(obj));
|
||||
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -1028,6 +1047,18 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
||||
|
||||
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
|
||||
fndef = (akbasic_FunctionDef *)fnptr;
|
||||
/*
|
||||
* A GEN is not a function: it yields more than once, through EMIT, and
|
||||
* nothing about an ordinary call ever resumes it for a second value.
|
||||
* Refused here, before anything is pushed, rather than left to fail
|
||||
* inside the call -- a BASIC-level error down in EMIT is swallowed by
|
||||
* process_line_run() the same way any statement's is, so a caller
|
||||
* driving its own step loop would see this "succeed" with whatever
|
||||
* garbage was left in the return slot instead of failing at all.
|
||||
*/
|
||||
FAIL_NONZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||
"%s is a GEN; call it with FOR EACH or DO EACH, not as a function",
|
||||
fndef->name);
|
||||
|
||||
/*
|
||||
* **One environment per call, from the pool -- exactly as GOSUB does.**
|
||||
|
||||
@@ -840,6 +840,31 @@ akerr_ErrorContext *akbasic_cmd_for(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
bool met = false;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
if ( obj->environment->isEachLoop ) {
|
||||
akbasic_Environment *loopenv = obj->environment;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (expr != NULL && akbasic_leaf_is_identifier(expr->right)),
|
||||
AKBASIC_ERR_SYNTAX, "Expected FOR EACH (variable) IN (generator call)");
|
||||
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
|
||||
"Expected FOR EACH (variable) IN (generator call)");
|
||||
|
||||
PASS(errctx, akbasic_environment_get(loopenv, expr->right->identifier,
|
||||
&loopenv->forNextVariable));
|
||||
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||
"Unable to get loop variable %s", expr->right->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 by waiting for NEXT,
|
||||
exactly as a zero-iteration plain FOR does. */
|
||||
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "NEXT"));
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
||||
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
@@ -891,10 +916,16 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
"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");
|
||||
if ( obj->environment->isEachLoop ) {
|
||||
/* EACH accepts any emitted type; the numeric-only check is for plain FOR. */
|
||||
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr->right), AKBASIC_ERR_SYNTAX,
|
||||
"Expected NEXT IDENTIFIER");
|
||||
} else {
|
||||
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;
|
||||
|
||||
@@ -909,6 +940,14 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"NEXT in an orphaned environment");
|
||||
/*
|
||||
* A live generator abandoned mid-run: release it too, or it never comes
|
||||
* back to the pool. See MAINTENANCE.md's note on abandoned generators.
|
||||
*/
|
||||
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||
obj->environment->forGeneratorEnv = NULL;
|
||||
}
|
||||
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
*dest = &obj->staticFalseValue;
|
||||
@@ -924,12 +963,37 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
if ( cmp != 0 ) {
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"NEXT in an orphaned environment");
|
||||
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||
obj->environment->forGeneratorEnv = NULL;
|
||||
}
|
||||
obj->environment->parent->nextline = obj->environment->nextline;
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
*dest = &obj->staticFalseValue;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
if ( obj->environment->isEachLoop ) {
|
||||
akbasic_Environment *loopenv = obj->environment;
|
||||
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||
if ( loopenv->forGeneratorEnv != NULL ) {
|
||||
obj->environment = loopenv->forGeneratorEnv;
|
||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||
}
|
||||
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||
/* Exhausted: pop the loop, same landing NEXT always uses when done. */
|
||||
FAIL_ZERO_RETURN(errctx, (loopenv->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"NEXT in an orphaned environment");
|
||||
loopenv->parent->nextline = loopenv->loopExitLine;
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
*dest = &obj->staticFalseValue;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
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);
|
||||
@@ -972,7 +1036,7 @@ akerr_ErrorContext *akbasic_cmd_exit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
*/
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED &&
|
||||
!obj->environment->isDoLoop),
|
||||
!obj->environment->isDoLoop && !obj->environment->isEachLoop),
|
||||
AKBASIC_ERR_STATE, "EXIT outside the context of FOR or DO");
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"EXIT in an orphaned environment");
|
||||
|
||||
297
src/runtime_generator.c
Normal file
297
src/runtime_generator.c
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* @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 ) {
|
||||
while ( obj->environment != loopenv && obj->environment->parent != NULL ) {
|
||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
@@ -80,6 +80,31 @@ akerr_ErrorContext *akbasic_cmd_do(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
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 ) {
|
||||
@@ -114,7 +139,21 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
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"));
|
||||
if ( loopenv->forGeneratorEnv != NULL ) {
|
||||
obj->environment = loopenv->forGeneratorEnv;
|
||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||
}
|
||||
again = (loopenv->forGeneratorEnv != NULL);
|
||||
} else {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||
/*
|
||||
|
||||
23
src/verbs.c
23
src/verbs.c
@@ -74,14 +74,32 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
||||
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
||||
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
|
||||
/*
|
||||
* EACH is never dispatched on its own -- akbasic_parse_for() and
|
||||
* akbasic_parse_do() consume it directly, the same way TO, STEP, WHILE and
|
||||
* UNTIL are. It exists here only so the scanner gives it a COMMAND token
|
||||
* rather than letting it scan as a plain identifier.
|
||||
*/
|
||||
{ "EACH", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "END", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end },
|
||||
{ "EMIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_emit },
|
||||
{ "END", AKBASIC_TOK_COMMAND, -1, akbasic_parse_end, akbasic_cmd_end },
|
||||
/*
|
||||
* `END GEN` is never scanned as one token -- END and GEN are ordinary
|
||||
* COMMAND tokens on the same line -- so this row is reached only from
|
||||
* akbasic_parse_end(), which builds a leaf carrying this exact name after
|
||||
* it sees GEN follow END. It still has to be here, and in order, because
|
||||
* dispatch is a bsearch on the leaf's name; the same reason INPUT# and
|
||||
* PRINT# are.
|
||||
*/
|
||||
{ "END GEN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end_gen },
|
||||
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
||||
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
|
||||
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
||||
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
|
||||
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
||||
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
||||
{ "GEN", AKBASIC_TOK_COMMAND, -1, akbasic_parse_gen, akbasic_cmd_gen },
|
||||
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
|
||||
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
|
||||
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
|
||||
@@ -94,6 +112,9 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
|
||||
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
|
||||
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
|
||||
/* IN is consumed directly by akbasic_parse_for()/akbasic_parse_do()'s EACH
|
||||
clause, the same way EACH itself is. */
|
||||
{ "IN", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
|
||||
/*
|
||||
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
|
||||
|
||||
@@ -20,6 +20,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_data(struct akbasic_Parser *par
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_graphic(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_draw(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_def(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_gen(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_end(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_dim(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_for(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_if(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||
@@ -111,6 +113,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_swap(struct akbasic_Runtime *obj,
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_troff(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tron(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
/* Group L generator verbs -- src/runtime_generator.c */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_emit(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_end_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
/* Verb handlers -- src/runtime_commands.c */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_auto(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_data(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
Reference in New Issue
Block a user