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:
44
README.md
44
README.md
@@ -254,7 +254,7 @@ Yes — in both directions, for integers, floats and strings, using the same var
|
||||
script itself uses. There is no marshalling layer and no copy: the host and the script are
|
||||
looking at the same `akbasic_Value`.
|
||||
|
||||
`akbasic_environment_get()` finds a variable by name and creates it if it does not exist. The
|
||||
`akbasic_runtime_global()` finds a variable by name and creates it if it does not exist. The
|
||||
type comes from the name's suffix, exactly as it does for BASIC code, so `HP#` is an integer
|
||||
and `NAME$` is a string. Every scalar is really a one-element array, which is why the subscript
|
||||
list is `{0}` with a count of 1.
|
||||
@@ -267,9 +267,7 @@ akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char
|
||||
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);
|
||||
}
|
||||
@@ -282,8 +280,7 @@ akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char
|
||||
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);
|
||||
@@ -310,26 +307,29 @@ CATCH(errctx, host_get_int(&SCRIPT, "SCORE#", &score)); /* collect */
|
||||
The full thing, including the string and float forms, is in
|
||||
[`examples/hostvars.c`](examples/hostvars.c). It is built and run by every build.
|
||||
|
||||
### The one rule: seed before, read after
|
||||
### Which scope it lands in, and why you should not care
|
||||
|
||||
**Do this between runs, not during one.** Seed before `akbasic_runtime_start()` and read after
|
||||
the script has stopped. Poking a variable while a script is suspended part-way through a
|
||||
bounded `akbasic_runtime_run()` is sharp in two ways, and both are silent:
|
||||
`akbasic_runtime_global()` always resolves against the script's outermost scope, so seeding
|
||||
before `akbasic_runtime_start()`, reading after the script stops, and creating a variable while
|
||||
a script is suspended part-way through a bounded `akbasic_runtime_run()` all do the same,
|
||||
obvious thing.
|
||||
|
||||
* A suspended script is usually inside a `FOR` or `GOSUB` scope, and `obj->environment` is that
|
||||
scope rather than the global one. A variable *created* there dies when the scope pops — the
|
||||
script reads it correctly inside the loop and gets `0` the moment the loop ends.
|
||||
* Reaching for the global scope explicitly does not help. Only the currently active environment
|
||||
auto-creates, so `akbasic_environment_get(root, "NEW#", &var)` with a child active hands back
|
||||
`NULL` and no error, and an unchecked host dereferences it.
|
||||
That is worth one paragraph of history, because the obvious-looking alternative is a trap.
|
||||
`akbasic_environment_get()` resolves against `obj->environment` — whatever scope is *active* —
|
||||
and a suspended script is usually inside a `FOR` or `GOSUB` body. A variable created through it
|
||||
lands in that body and dies when the body pops: the script reads it correctly inside the loop
|
||||
and gets `0` the moment the loop ends, with nothing raised anywhere. Reaching for the root
|
||||
explicitly does not help either, because only the active environment auto-creates, so
|
||||
`akbasic_environment_get(root, "NEW#", &var)` with a child active hands back `NULL` and no
|
||||
error and an unchecked host dereferences it.
|
||||
|
||||
Reading and *updating an existing* global is safe at any time — the parent chain is searched,
|
||||
so a variable that already exists in the global scope is found from anywhere. It is only
|
||||
*creation* that lands in the wrong place.
|
||||
Both of those are still true of `akbasic_environment_get()`, which is the right behaviour for
|
||||
what the *interpreter* uses it for. They are simply not your problem: use
|
||||
`akbasic_runtime_global()`.
|
||||
|
||||
`examples/hostvars.c` demonstrates both hazards rather than describing them. This is filed as
|
||||
[`TODO.md`](TODO.md) section 6 item 17; the fix is a small `akbasic_runtime_global()` that
|
||||
resolves against the root scope regardless of what is active.
|
||||
[`examples/hostvars.c`](examples/hostvars.c) shows the difference rather than describing it,
|
||||
and `tests/hostvars.c` asserts it — a global created from inside a `FOR` body, and from inside
|
||||
a `GOSUB`, and still readable by the script afterwards.
|
||||
|
||||
## Where the output goes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user