All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
99 lines
3.6 KiB
C
99 lines
3.6 KiB
C
/**
|
|
* @file verbs_table.c
|
|
* @brief Tests the dispatch table's invariants.
|
|
*
|
|
* The sorting assertion is the important one: bsearch on a mis-sorted table
|
|
* silently fails to find a verb rather than producing a compile error, and the
|
|
* symptom would be "Unknown command PRINT" a long way from the cause.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/verbs.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
int main(void)
|
|
{
|
|
const akbasic_Verb *table = NULL;
|
|
const akbasic_Verb *found = NULL;
|
|
int count = 0;
|
|
int i = 0;
|
|
|
|
TEST_REQUIRE_OK(akbasic_error_register());
|
|
|
|
table = akbasic_verb_table(&count);
|
|
TEST_REQUIRE(table != NULL, "the verb table must exist");
|
|
TEST_REQUIRE(count > 0, "the verb table must not be empty");
|
|
|
|
/* Sorted, strictly: bsearch requires it and a duplicate name is a bug. */
|
|
for ( i = 1; i < count; i++ ) {
|
|
TEST_REQUIRE(strcmp(table[i - 1].name, table[i].name) < 0,
|
|
"table is not sorted at index %d: \"%s\" then \"%s\"",
|
|
i, table[i - 1].name, table[i].name);
|
|
}
|
|
|
|
/* Every row is reachable by its own name. */
|
|
for ( i = 0; i < count; i++ ) {
|
|
TEST_REQUIRE_OK(akbasic_verb_lookup(table[i].name, &found));
|
|
TEST_REQUIRE(found == &table[i], "row \"%s\" is not findable", table[i].name);
|
|
}
|
|
|
|
/* Verbs and function names are case-insensitive; variable names are not. */
|
|
TEST_REQUIRE_OK(akbasic_verb_lookup("print", &found));
|
|
TEST_REQUIRE(found != NULL && strcmp(found->name, "PRINT") == 0, "lowercase print missed");
|
|
TEST_REQUIRE_OK(akbasic_verb_lookup("PrInT", &found));
|
|
TEST_REQUIRE(found != NULL && strcmp(found->name, "PRINT") == 0, "mixed-case print missed");
|
|
|
|
/* A miss is a NULL, not an error. */
|
|
TEST_REQUIRE_OK(akbasic_verb_lookup("NOTAVERB", &found));
|
|
TEST_REQUIRE(found == NULL, "an unknown name must miss");
|
|
TEST_REQUIRE_OK(akbasic_verb_lookup("", &found));
|
|
TEST_REQUIRE(found == NULL, "an empty name must miss");
|
|
|
|
/* Every function row carries a real arity and a handler. */
|
|
for ( i = 0; i < count; i++ ) {
|
|
if ( table[i].tokentype != AKBASIC_TOK_FUNCTION ) {
|
|
continue;
|
|
}
|
|
TEST_REQUIRE(table[i].arity >= 1,
|
|
"function \"%s\" has arity %d", table[i].name, table[i].arity);
|
|
TEST_REQUIRE(table[i].exec != NULL,
|
|
"function \"%s\" has no exec handler", table[i].name);
|
|
}
|
|
|
|
/*
|
|
* These rows have no exec handler because another verb's parse path consumes
|
|
* them; they are never evaluated on their own. `WHILE` and `UNTIL` belong to
|
|
* DO and LOOP the way `TO` and `STEP` belong to FOR, and `EACH`/`IN` belong
|
|
* to FOR EACH and DO EACH the same way. Any other missing handler is a verb
|
|
* that would report "Unknown command" at runtime.
|
|
*/
|
|
for ( i = 0; i < count; i++ ) {
|
|
if ( table[i].exec != NULL ) {
|
|
continue;
|
|
}
|
|
TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 ||
|
|
strcmp(table[i].name, "EACH") == 0 ||
|
|
strcmp(table[i].name, "ELSE") == 0 ||
|
|
strcmp(table[i].name, "IN") == 0 ||
|
|
strcmp(table[i].name, "NOT") == 0 ||
|
|
strcmp(table[i].name, "OR") == 0 ||
|
|
strcmp(table[i].name, "REM") == 0 ||
|
|
strcmp(table[i].name, "STEP") == 0 ||
|
|
strcmp(table[i].name, "UNTIL") == 0 ||
|
|
strcmp(table[i].name, "USING") == 0 ||
|
|
strcmp(table[i].name, "WHILE") == 0 ||
|
|
strcmp(table[i].name, "THEN") == 0 ||
|
|
strcmp(table[i].name, "TO") == 0,
|
|
"verb \"%s\" has no exec handler and is not a known passive token",
|
|
table[i].name);
|
|
}
|
|
|
|
TEST_REQUIRE_STATUS(akbasic_verb_lookup(NULL, &found), AKERR_NULLPOINTER);
|
|
TEST_REQUIRE_STATUS(akbasic_verb_lookup("PRINT", NULL), AKERR_NULLPOINTER);
|
|
|
|
return akbasic_test_failures;
|
|
}
|