Add akbasic_runtime_global: a host variable lands in the script's root scope
A host creating a variable while a script was suspended got it in whatever scope was active -- usually a FOR or GOSUB body -- and it died when the body popped, silently. Reaching for the root by hand returned NULL without raising, because environment_get only auto-creates in the active environment. Both are still true of environment_get, which is correct for what the interpreter uses it for. The README and the example now point somewhere else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,16 @@
|
||||
* kept here so it is compiled and run by every build rather than rotting in a
|
||||
* document.
|
||||
*
|
||||
* The safe pattern is: seed variables *before* akbasic_runtime_start(), read
|
||||
* results *after* the script stops. Poking a variable while the script is
|
||||
* suspended part-way through is sharp in two ways that this file demonstrates at
|
||||
* the bottom -- see TODO.md section 6 item 17.
|
||||
* **Use akbasic_runtime_global().** It finds or creates the variable in the
|
||||
* script's outermost scope, which is what a host means every time. The obvious
|
||||
* alternative, akbasic_environment_get(), lands a new variable in whatever scope
|
||||
* is *active* -- and a script suspended part-way through a bounded run() is
|
||||
* usually inside a FOR or GOSUB body, so the variable dies when that body pops
|
||||
* and the script reads zero from then on, with nothing raised anywhere.
|
||||
*
|
||||
* The bottom of this file demonstrates that difference rather than describing
|
||||
* it, because it was TODO.md section 6 item 17 and it is the sort of thing that
|
||||
* gets rediscovered.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -33,7 +39,7 @@ static const char *PROGRAM =
|
||||
/* ------------------------------------------------------- host -> script -- */
|
||||
|
||||
/*
|
||||
* Seed an integer the script can read. akbasic_environment_get() creates the
|
||||
* Seed an integer the script can read. akbasic_runtime_global() creates the
|
||||
* variable if it does not exist, and the type comes from the name's suffix, so
|
||||
* "HP#" is an integer and "HP$" would be a string.
|
||||
*
|
||||
@@ -46,9 +52,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, con
|
||||
akbasic_Variable *variable = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY,
|
||||
"could not reach variable %s from the active scope", name);
|
||||
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
|
||||
PASS(errctx, akbasic_variable_set_integer(variable, value, subscript, 1));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -59,9 +63,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *host_set_string(akbasic_Runtime *obj,
|
||||
akbasic_Variable *variable = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY,
|
||||
"could not reach variable %s from the active scope", name);
|
||||
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
|
||||
PASS(errctx, akbasic_variable_set_string(variable, value, subscript, 1));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -75,8 +77,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, con
|
||||
akbasic_Value *value = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY, "no variable %s", name);
|
||||
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
|
||||
PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
||||
"%s is not an integer", name);
|
||||
@@ -91,8 +92,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *host_get_string(akbasic_Runtime *obj,
|
||||
akbasic_Value *value = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY, "no variable %s", name);
|
||||
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
|
||||
PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||||
"%s is not a string", name);
|
||||
@@ -105,9 +105,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *host_get_string(akbasic_Runtime *obj,
|
||||
/* ------------------------------------------------------------- the demo -- */
|
||||
|
||||
/*
|
||||
* The hazard, demonstrated rather than described. A script suspended part-way
|
||||
* through is usually inside a FOR or GOSUB scope, and a variable created there
|
||||
* dies when that scope pops.
|
||||
* The difference between the two entry points, demonstrated rather than
|
||||
* described. The script prints the same variable from inside a loop and from
|
||||
* after it, so a variable that died with the loop's scope is visible as a zero
|
||||
* on the second line rather than as an error anywhere.
|
||||
*/
|
||||
static const char *LOOPING_PROGRAM =
|
||||
"10 FOR I# = 1 TO 3\n"
|
||||
@@ -115,7 +116,24 @@ static const char *LOOPING_PROGRAM =
|
||||
"30 NEXT I#\n"
|
||||
"40 PRINT \" after loop, GIFT# = \" + GIFT#\n";
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *demonstrate_scope_hazard(void)
|
||||
/* Step until the script is inside the loop body, whatever line that turns out
|
||||
* to be. A step processes one source-line *index*, and lines are filed under
|
||||
* their BASIC line numbers, so a fixed count would be a hostage to them. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_into_the_loop(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int steps = 0;
|
||||
|
||||
while ( steps < AKBASIC_MAX_SOURCE_LINES &&
|
||||
SCRIPT.mode != AKBASIC_MODE_QUIT &&
|
||||
SCRIPT.environment->parent == NULL ) {
|
||||
PASS(errctx, akbasic_runtime_run(&SCRIPT, 1));
|
||||
steps += 1;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *demonstrate_scope_rules(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Environment *root = NULL;
|
||||
@@ -126,18 +144,25 @@ static akerr_ErrorContext AKERR_NOIGNORE *demonstrate_scope_hazard(void)
|
||||
PASS(errctx, akbasic_runtime_load(&SCRIPT, LOOPING_PROGRAM));
|
||||
PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
||||
|
||||
/* Run far enough to be inside the loop body. */
|
||||
PASS(errctx, akbasic_runtime_run(&SCRIPT, 12));
|
||||
PASS(errctx, run_into_the_loop());
|
||||
printf("suspended inside the loop? %s\n",
|
||||
(SCRIPT.environment != root ? "yes" : "no"));
|
||||
|
||||
/* Poking through the *active* scope creates the variable in the loop. */
|
||||
/*
|
||||
* The right way. The variable lands in the root scope, so line 40 -- which
|
||||
* runs after NEXT has popped the loop -- still sees it.
|
||||
*/
|
||||
PASS(errctx, host_set_int(&SCRIPT, "GIFT#", 42));
|
||||
|
||||
/* Reaching for the root scope while a child is active simply misses. */
|
||||
/*
|
||||
* The wrong way, kept because the failure is silent and worth seeing once.
|
||||
* akbasic_environment_get() only auto-creates in the *active* environment,
|
||||
* so reaching for the root while a child is active returns NULL through
|
||||
* dest without raising -- and an unchecked host dereferences it.
|
||||
*/
|
||||
PASS(errctx, akbasic_environment_get(root, "UNREACHABLE#", &variable));
|
||||
printf("can the host create in the root scope mid-run? %s\n",
|
||||
(variable != NULL ? "yes" : "no -- get() returns NULL, with no error"));
|
||||
printf("does environment_get() create in the root mid-run? %s\n",
|
||||
(variable != NULL ? "yes" : "no -- it returns NULL, with no error"));
|
||||
|
||||
PASS(errctx, akbasic_runtime_run(&SCRIPT, 0));
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -172,8 +197,8 @@ int main(void)
|
||||
printf("host read back: SCORE# = %lld, STATUS$ = \"%s\"\n",
|
||||
(long long)score, status);
|
||||
|
||||
printf("\n--- the mid-run scope hazard (TODO.md 6.17) ---\n");
|
||||
CATCH(errctx, demonstrate_scope_hazard());
|
||||
printf("\n--- creating a variable mid-run ---\n");
|
||||
CATCH(errctx, demonstrate_scope_rules());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
|
||||
Reference in New Issue
Block a user