Enter a TRAP handler when the variable table is full

Two separable faults, both on the path a program takes when it is already in
trouble.

`akbasic_trap_set_error_variables()` reached `ER#` and `EL#` through
`akbasic_runtime_global()`, which *creates* a name the program never used -- and
creating one takes a variable slot. So a program that had filled the 128-slot
table could not have its handler entered at all, and because the failure
happened inside the error path rather than raising, nothing was reported and the
program carried on with the failing statement's effect quietly missing. A wrong
answer delivered as a right one, which is worse than an abort.

`akbasic_runtime_reserve_globals()` now creates both at runtime init, where
there is always room, and `clear_variables()` puts them back after `CLR` and
`NEW` empty the table.

Second: `report_and_reraise()` used a plain `PASS` around the report, so a
failure while reporting *replaced* the error the program had actually made --
"Maximum runtime variables reached" in place of the subscript that was out of
range. The secondary failure is now logged and the original is re-raised, which
is what a user needs to hear.

**The reduction in TODO.md no longer reproduces, and not because of this.** The
value-pool fix in the previous commit made a scalar free, so the pool can no
longer be emptied by creating names. The defect was still live through the
variable table: 124 names and a TRAP armed, and the handler was silently
skipped. That is what the new test in tests/trap_verbs.c pins, together with the
invariant -- both globals present before a program runs.

Verified by reverting the fix against the new tests: both fail, and the second
prints the log line the swallow used to eat, "could not report a BASIC error 515
(Out Of Bounds): Maximum runtime variables reached".

TODO.md section 6 item 33, struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:48:56 -04:00
parent 05f241aca1
commit f5a35b4af6
5 changed files with 159 additions and 3 deletions

View File

@@ -222,6 +222,75 @@ static void test_err_function(void)
harness_stop();
}
/**
* @brief `ER#` and `EL#` exist before a program runs, so the dispatch cannot fail.
*
* The dispatch used to reach them through akbasic_runtime_global(), which
* *creates* a name the program never used -- and creating one takes a variable
* slot. So the handler could not be entered by a program that had filled the
* table, which is the one kind of program most likely to raise. Asserted here as
* the invariant rather than only through the symptom below, because the symptom
* needs 124 lines of setup and this needs none.
*/
static void test_error_globals_are_reserved(void)
{
bool haveer = false;
bool haveel = false;
int i = 0;
TEST_REQUIRE_OK(harness_start(NULL));
for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) {
if ( !HARNESS_RUNTIME.variables[i].used ) {
continue;
}
if ( strcmp(HARNESS_RUNTIME.variables[i].name, "ER#") == 0 ) {
haveer = true;
}
if ( strcmp(HARNESS_RUNTIME.variables[i].name, "EL#") == 0 ) {
haveel = true;
}
}
TEST_REQUIRE(haveer, "ER# must exist as soon as the runtime is initialised");
TEST_REQUIRE(haveel, "EL# must exist as soon as the runtime is initialised");
harness_stop();
}
/**
* @brief A handler is entered even by a program that has filled the variable table.
*
* TODO.md section 6 item 33, and much worse than an abort: the dispatch failed
* *inside* the error path rather than raising, so nothing was reported, the
* handler never ran, and the program carried on with the failing statement's
* effect quietly missing -- a wrong answer delivered as a right one.
*
* 124 names, which leaves the table with no room to invent `ER#` and `EL#` at
* the moment they are wanted. It is 124 rather than 128 because the runtime
* reserves those two and the loop counter is a third.
*/
static void test_handler_runs_with_a_full_variable_table(void)
{
char source[AKBASIC_MAX_VARIABLES * 32];
size_t used = 0;
int i = 0;
used = (size_t)snprintf(source, sizeof(source), "10 TRAP CAUGHT\n");
for ( i = 0; i < 124; i++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used,
"%d V%03d# = %d\n", 20 + i, i, i);
}
snprintf(source + used, sizeof(source) - used,
"800 DIM TOOBIG#(5000)\n"
"810 PRINT \"NOT TRAPPED\"\n"
"820 END\n"
"900 LABEL CAUGHT\n"
"910 PRINT \"TRAPPED\"\n"
"920 END\n");
TEST_REQUIRE_OK(run_program(source));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TRAPPED\n");
harness_stop();
}
int main(void)
{
test_trap_catches();
@@ -233,5 +302,7 @@ int main(void)
test_resume_outside_handler();
test_error_inside_handler();
test_err_function();
test_error_globals_are_reserved();
test_handler_runs_with_a_full_variable_table();
return akbasic_test_failures;
}