Merge branch 'main' into 36
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m25s
akbasic CI Build / coverage (push) Successful in 4m18s
akbasic CI Build / sanitizers (push) Successful in 5m4s
akbasic CI Build / akgl_build (push) Successful in 8m17s
akbasic CI Build / mutation_test (push) Successful in 23m54s

This commit is contained in:
2026-08-06 12:38:05 -04:00
committed by Starfort Source Vault
71 changed files with 2145 additions and 302 deletions

View File

@@ -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;

View File

@@ -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,58 @@ 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));
/*
* Same guard as akbasic_parse_for()'s EACH branch, with the likely
* mistake named: a condition belongs on the LOOP, where it is
* checked against each emitted value, not here on the DO.
*/
peeked = akbasic_parser_peek(parser);
FAIL_NONZERO_RETURN(errctx,
(peeked != NULL &&
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
peeked->tokentype != AKBASIC_TOK_COLON),
AKBASIC_ERR_SYNTAX,
"DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here");
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 +936,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 +1079,72 @@ 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));
/*
* Nothing may follow the generator call but another statement.
* Without this, a stray clause sits unparsed on the line and only
* blows up after the whole loop has run, when the parent scope
* resumes the line mid-statement -- an error at the loop's end
* pointing at its beginning.
*/
peeked = akbasic_parser_peek(parser);
FAIL_NONZERO_RETURN(errctx,
(peeked != NULL &&
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
peeked->tokentype != AKBASIC_TOK_COLON),
AKBASIC_ERR_SYNTAX,
"FOR EACH takes nothing after the generator call");
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,

View File

@@ -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,51 @@ 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);
}
akerr_ErrorContext *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target)
{
PREPARE_ERROR(errctx);
akbasic_Environment *popped = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && target != NULL), AKERR_NULLPOINTER,
"NULL argument in unwind_to_environment");
/*
* Stops early at the root rather than failing on it: every caller is an
* error-unwind path, where "release whatever there is" beats raising a
* second failure on top of the one being cleaned up after.
*/
while ( obj->environment != target && obj->environment->parent != NULL ) {
popped = obj->environment;
obj->environment = popped->parent;
/*
* An EACH loop scope on its way out takes its suspended generator with
* it -- the generator is a *child* of the scope, off the parent chain,
* and this walk is the only thing that will ever see it again. Guarded
* on `used` because a generator that was being pumped when the failure
* hit is *on* the chain being unwound, already released by the time
* the walk reaches the loop scope that references it.
*/
if ( popped->forGeneratorEnv != NULL && popped->forGeneratorEnv->used ) {
PASS(errctx, akbasic_runtime_release_generator(obj, popped->forGeneratorEnv));
}
popped->forGeneratorEnv = NULL;
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
}
SUCCEED_RETURN(errctx);
}
@@ -1029,6 +1080,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.**
@@ -1136,10 +1199,11 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
* Give them back, or a host absorbing script errors drains the
* twelve-slot environment pool after twelve dead calls and every
* call after that fails for a reason nobody can see in the script.
* The unwind, not a bare prev_environment() loop, because a body that
* died inside a FOR EACH leaves a suspended generator hanging off the
* loop scope, and only the unwind knows to take it down too.
*/
while ( obj->environment != targetenv && obj->environment->parent != NULL ) {
IGNORE(akbasic_runtime_prev_environment(obj));
}
IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv));
} PROCESS(errctx) {
} FINISH(errctx, true);
PASS(errctx, akbasic_environment_new_value(targetenv, &out));

View File

@@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
/*
* A GEN is a function at heart, and RETURN ends it the way it ends a DEF
* or a GOSUB: early, cleanly, from its own frame. What a generator's
* RETURN cannot do is carry a value -- values leave a GEN one at a time,
* through EMIT, and there is no caller waiting on a return slot.
*/
if ( obj->environment->isGenerator ) {
FAIL_NONZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_STATE,
"A GEN yields values through EMIT; RETURN here takes none");
PASS(errctx, akbasic_runtime_prev_environment(obj));
obj->environment->forGeneratorEnv = NULL;
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
"RETURN outside the context of GOSUB");
"RETURN outside the context of GOSUB, DEF, or GEN");
if ( expr != NULL && expr->right != NULL ) {
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
@@ -840,6 +854,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 +930,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 +954,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 +977,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 +1050,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");

View File

@@ -407,17 +407,22 @@ akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in DIRECTORY");
/*
* Refused rather than half-built. Listing a directory needs opendir/readdir,
* which `libakstdlib` does not wrap -- and this project's rule is that a
* missing capability gets filed upstream rather than worked around here
* (MAINTENANCE.md). Filed as libakstdlib issue #10.
* Refused rather than half-built. This was blocked upstream: listing a
* directory needs opendir/readdir, `libakstdlib` did not wrap them, and
* this project's rule is that a missing capability gets filed upstream
* rather than worked around here (MAINTENANCE.md). That was libakstdlib
* issue #10, and it landed -- aksl_opendir, aksl_readdir, aksl_closedir
* and aksl_rewinddir all exist as of the revision this tree pins.
*
* The alternative was shelling out to `ls`, which a library has no business
* doing, or calling readdir directly and stepping outside the error
* convention every other call in this file follows.
* So the blocker is gone and only the work is left. Writing the verb needs
* decisions this commit is not the place for: what a listing looks like on
* a filesystem with no disk-image block counts, which of the Commodore
* wildcard forms to honour, and where the entries go. Tracked as akbasic
* issue #55; the refusal stays honest until then rather than growing a
* half-listing nobody specified.
*/
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper yet");
"DIRECTORY is not implemented yet");
}
/* ------------------------------------------------------------ BSAVE/BLOAD -- */

View File

@@ -183,6 +183,69 @@ akerr_ErrorContext *akbasic_fn_chr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_asc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
const unsigned char *text = NULL;
int64_t codepoint = 0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "ASC", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"ASC expected a string");
FAIL_ZERO_RETURN(errctx, (arg->stringval[0] != '\0'), AKBASIC_ERR_BOUNDS,
"ASC expected a non-empty string");
/* Decode the first UTF-8 code point, the inverse of CHR's encoder. */
text = (const unsigned char *)arg->stringval;
if ( text[0] < 0x80 ) {
codepoint = text[0];
} else if ( (text[0] & 0xE0) == 0xC0 ) {
codepoint = ((int64_t)(text[0] & 0x1F) << 6) |
(text[1] & 0x3F);
} else if ( (text[0] & 0xF0) == 0xE0 ) {
codepoint = ((int64_t)(text[0] & 0x0F) << 12) |
((int64_t)(text[1] & 0x3F) << 6) |
(text[2] & 0x3F);
} else {
codepoint = ((int64_t)(text[0] & 0x07) << 18) |
((int64_t)(text[1] & 0x3F) << 12) |
((int64_t)(text[2] & 0x3F) << 6) |
(text[3] & 0x3F);
}
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = codepoint;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_rnd(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
const int64_t modulus = 2147483648;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "RND", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"RND expected an integer");
FAIL_ZERO_RETURN(errctx, (arg->intval > 0), AKBASIC_ERR_VALUE,
"RND count %" PRId64 " must be positive", arg->intval);
if ( !obj->rndseeded ) {
obj->rndseed = obj->timems % modulus;
obj->rndseeded = true;
}
obj->rndseed = (obj->rndseed * 1103515245 + 12345) % modulus;
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (obj->rndseed / 65536) % arg->intval;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);

307
src/runtime_generator.c Normal file
View File

@@ -0,0 +1,307 @@
/**
* @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);
}

View File

@@ -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,37 @@ 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"));
/*
* 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"));
/*

View File

@@ -48,6 +48,7 @@ static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest)
/**
* @brief The character under the cursor.
* @param obj The runtime whose scan cursor is being read.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
@@ -70,6 +71,7 @@ static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got)
/**
* @brief The character one past the cursor.
* @param obj The runtime whose scan cursor is being read.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
@@ -145,6 +147,10 @@ static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType tok
/**
* @brief Consume one more character when it matches, choosing between two token types.
* @param obj The runtime whose scan cursor is being advanced.
* @param cm The character that must be next for the match to succeed.
* @param truetype The token type to report when @p cm matches.
* @param falsetype The token type to report when it does not.
* @param[out] matched Whether the character was consumed. The old `bool` return.
*
* On the chain below `peek`, so it reports the same way. See libakstdlib #38.

View File

@@ -875,7 +875,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
* proxy carries the owner only so a resolver can push something. Nothing here
* resolves anything, so the field stays empty and the shape is what matters.
*
* Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and
* Layers are the other half. A wall sits on `AKGL_COLLISION_LAYER_STATIC` and
* responds to nothing, which is the asymmetry libakgl's masks exist for: the
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
* walls never test against each other. Sixty-four motionless rectangles

View File

@@ -37,6 +37,7 @@ static const akbasic_Verb VERBS[] = {
{ "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs },
{ "AND", AKBASIC_TOK_AND, -1, NULL, NULL },
{ "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append },
{ "ASC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_asc },
{ "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn },
{ "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto },
{ "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup },
@@ -73,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 },
@@ -93,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
@@ -146,6 +168,7 @@ static const akbasic_Verb VERBS[] = {
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
{ "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu },
{ "RND", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rnd },
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
{ "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit },
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },

View File

@@ -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);
@@ -140,6 +147,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_stop(struct akbasic_Runtime *obj,
/* Function handlers -- src/runtime_functions.c */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_abs(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_asc(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_atn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_chr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_cos(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
@@ -154,6 +162,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_peek(struct akbasic_Runtime *obj,
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointer(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointervar(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rad(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rnd(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_right(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_sgn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_shl(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);