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:
68
TODO.md
68
TODO.md
@@ -895,38 +895,35 @@ question.
|
||||
|
||||
### Ours, not the reference's
|
||||
|
||||
17. **A host cannot reliably create a script variable while a script is suspended.** This one
|
||||
is not inherited — it falls out of §1.4's environment pool meeting §1.6's bounded `run()`,
|
||||
a combination the reference never had because its `run()` never returned.
|
||||
17. ~~**A host cannot reliably create a script variable while a script is suspended.**~~
|
||||
**Fixed.** `akbasic_runtime_global(obj, name, dest)` walks `obj->environment` to the root
|
||||
and finds or creates there unconditionally. `README.md` and `examples/hostvars.c` both point
|
||||
at it now, and `tests/hostvars.c` asserts it: a global created from inside a `FOR` body and
|
||||
from inside a `GOSUB`, still readable by the script after the body pops, plus the
|
||||
seed-before / read-after path that always worked.
|
||||
|
||||
Exchanging variables with an embedding host works and is documented in `README.md`: the
|
||||
host calls `akbasic_environment_get()` to find or create a variable and
|
||||
`akbasic_variable_set_*` / `_get_subscript` to move values across. Seeding before
|
||||
`akbasic_runtime_start()` and reading after the script stops is safe, and so is *updating*
|
||||
an existing global at any time, because the parent chain is searched. **Creating** one
|
||||
mid-run is not, in two ways, and both are silent:
|
||||
This one was not inherited — it fell out of §1.4's environment pool meeting §1.6's bounded
|
||||
`run()`, a combination the reference never had because its `run()` never returned. Both
|
||||
failure modes were silent: a variable created through `akbasic_environment_get()` landed in
|
||||
the loop's scope and died with it, so the script read it correctly inside the loop and got
|
||||
`0` immediately after; and reaching for the root explicitly returned `NULL` through `dest`
|
||||
**without raising**, because that function only auto-creates when
|
||||
`obj->runtime->environment == obj`.
|
||||
|
||||
- A script suspended part-way through a bounded `akbasic_runtime_run()` is usually inside a
|
||||
`FOR` or `GOSUB` scope, so `obj->environment` is that scope. A variable created through
|
||||
it lands in the loop's scope and dies when the environment pops — the script reads the
|
||||
value correctly inside the loop and gets `0` immediately after it.
|
||||
- Reaching for the root explicitly does not help. `akbasic_environment_get` only
|
||||
auto-creates when `obj->runtime->environment == obj` (`src/environment.c:242`), so with a
|
||||
child active it returns `NULL` through `dest` **without raising**, and an unchecked host
|
||||
dereferences it.
|
||||
Three things settled while doing it, each worth keeping:
|
||||
|
||||
`examples/hostvars.c` demonstrates both rather than describing them, and prints what it
|
||||
observes, so the day this is fixed that output changes and the example needs revisiting.
|
||||
|
||||
Fix: add `akbasic_runtime_global(akbasic_Runtime *obj, const char *name, akbasic_Variable **dest)`
|
||||
that walks `obj->environment` to the root and creates there unconditionally, and point the
|
||||
README at it instead of at `akbasic_environment_get`. Roughly fifteen lines. The one design
|
||||
question worth settling first is what it should do when the script is suspended inside a
|
||||
*user function's* scope — that environment is owned by the funcdef rather than the pool
|
||||
(§1.4), and "root" is still the right answer there, but it is worth being deliberate about
|
||||
it rather than discovering it. Tests: create a global while suspended in a `FOR` body and
|
||||
assert the script still sees it after `NEXT`; the same from inside a `GOSUB`; and the
|
||||
NULL-without-error path, which should become an impossible state.
|
||||
- **The design question §6 named has an answer.** Suspended inside a *user function's*
|
||||
scope, the root is still right and the walk still gets there: a funcdef's environment is
|
||||
initialized with the caller's as its parent (`akbasic_runtime_user_function`), so the
|
||||
chain terminates at the same root as any other.
|
||||
- **`akbasic_environment_create()` is the new half**, and it searches *only* the scope it
|
||||
is given — no walk in either direction. Walking up would find an outer variable and put
|
||||
the value somewhere other than where the caller asked for it.
|
||||
- **`akbasic_environment_get()` is unchanged.** Declining to create in a non-active scope is
|
||||
the right behaviour for what the interpreter uses it for; it is only wrong as a *host*
|
||||
API, which is why the answer was a new entry point rather than a change to that one.
|
||||
`tests/hostvars.c` asserts the old behaviour too, so a later "tidy-up" has to argue with a
|
||||
test.
|
||||
|
||||
---
|
||||
|
||||
@@ -1019,10 +1016,10 @@ entire test corpus and passes clean under ASan and UBSan.
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `ctest` | 74/74 — 41 upstream golden cases, 8 local ones, 22 unit tests, 2 embedding examples, 1 known-failing |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 73/73 — the same, minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends` and `akgl_frontend`; the `akgl_build` CI job |
|
||||
| `ctest` | 75/75 — 41 upstream golden cases, 8 local ones, 23 unit tests, 2 embedding examples, 1 known-failing |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 74/74 — the same, minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends` and `akgl_frontend`; the `akgl_build` CI job |
|
||||
| Golden corpus | 41/41 byte-exact, driven in place from the submodule — **and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output |
|
||||
| ASan + UBSan | 74/74 |
|
||||
| ASan + UBSan | 75/75 |
|
||||
| Line coverage | 93.6% (3618/3867) — above the 90% gate |
|
||||
| Function coverage | 97.8% (267/273) |
|
||||
| Warnings | none under `-Wall -Wextra` |
|
||||
@@ -1137,14 +1134,13 @@ What remains, in priority order:
|
||||
deliberately does not clone. Two apt packages against two more submodules is an easy trade.
|
||||
|
||||
Every step was verified from a clean clone before being written down, not inferred.
|
||||
5. **§6 items 16 and 17.** Items 10 and 12–15 are fixed, each with a regression test in the
|
||||
5. **§6 item 16.** Items 10, 12–15 and 17 are fixed, each with a regression test in the
|
||||
suite that pinned it. What is left is not more of the same:
|
||||
- **Item 16** is a *decision*, not a fix. Making the "Reserved word in variable name" check
|
||||
fire costs an upstream golden case, because the reference's own `strreverse.bas` names a
|
||||
variable `INPUT$`. §6 states the trade; somebody has to make it.
|
||||
- **Item 17** is ours: a host cannot reliably create a script variable while a script is
|
||||
suspended. The fix is `akbasic_runtime_global()` and roughly fifteen lines. §6 describes
|
||||
it and names the one design question to settle first.
|
||||
- ~~**Item 17**~~ **Done** — `akbasic_runtime_global()`, `tests/hostvars.c`, and the README
|
||||
and example both pointed at it.
|
||||
6. **Mutation survivors in `src/value.c`** — ~~closed~~, with one correction to what was
|
||||
written here before. `tests/value_arithmetic.c` grew two functions,
|
||||
`test_maximum_length_string()` and `test_pool_starts_empty()`, and each was verified by
|
||||
|
||||
Reference in New Issue
Block a user