Merge branch 'main' into 10
Some checks failed
akbasic CI Build / mutation_test (push) Failing after 11m28s
akbasic CI Build / akgl_build (push) Failing after 11m46s
akbasic CI Build / coverage (push) Failing after 12m10s
akbasic CI Build / sanitizers (push) Failing after 12m21s
akbasic CI Build / cmake_build (push) Failing after 12m33s
Some checks failed
akbasic CI Build / mutation_test (push) Failing after 11m28s
akbasic CI Build / akgl_build (push) Failing after 11m46s
akbasic CI Build / coverage (push) Failing after 12m10s
akbasic CI Build / sanitizers (push) Failing after 12m21s
akbasic CI Build / cmake_build (push) Failing after 12m33s
This commit is contained in:
@@ -244,10 +244,16 @@ static void test_no_drive_verbs(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/* DIRECTORY is refused for a different reason, and says which. */
|
||||
/*
|
||||
* DIRECTORY is refused for a different reason than the five above: not for
|
||||
* want of a drive, but because it is unwritten. It used to name the missing
|
||||
* libakstdlib wrapper; that wrapper landed, so naming it would be a lie.
|
||||
*/
|
||||
TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") != NULL,
|
||||
"DIRECTORY should name the missing wrapper, got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not implemented") != NULL,
|
||||
"DIRECTORY should say it is unwritten, got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") == NULL,
|
||||
"DIRECTORY must not still blame libakstdlib, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
|
||||
360
tests/generators.c
Normal file
360
tests/generators.c
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* @file generators.c
|
||||
* @brief Generators: GEN/EMIT/END GEN and the FOR EACH/DO EACH loops over them.
|
||||
*
|
||||
* The golden corpus (tests/language/flowcontrol/generators_*.bas) covers the
|
||||
* ordinary shapes: the issue's own ROOMOBJECTS example in both loop forms, an
|
||||
* empty generator, a non-numeric EMIT and nested/interleaved invocations. This
|
||||
* file covers what a byte-compared program cannot: that abandoning a
|
||||
* generator with EXIT gives its environment back to the pool rather than
|
||||
* leaking it, and that misusing a GEN fails cleanly rather than corrupting the
|
||||
* environment stack.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
|
||||
#include "harness.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Run a program to completion under an explicit step budget. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, harness_start(NULL));
|
||||
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||||
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, run_program_bounded(source, 20000));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief The issue's own example, as a sanity check independent of the golden corpus. */
|
||||
static void test_room_objects_smoke(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 DIM OBJ#(3)\n"
|
||||
"20 OBJ#(0) = 100\n"
|
||||
"30 OBJ#(1) = 200\n"
|
||||
"40 OBJ#(2) = 300\n"
|
||||
"50 GEN ROOMOBJECTS(R#)\n"
|
||||
"60 FOR I# = 0 TO 2\n"
|
||||
"70 IF I# <> 1 THEN EMIT OBJ#(I#)\n"
|
||||
"80 NEXT I#\n"
|
||||
"90 END GEN\n"
|
||||
"100 FOR EACH O# IN ROOMOBJECTS(0)\n"
|
||||
"110 PRINT O#\n"
|
||||
"120 NEXT O#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "100\n300\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT out of a FOR EACH loop releases its generator, not just the loop.
|
||||
*
|
||||
* Run more FOR EACH/EXIT constructs than AKBASIC_MAX_ENVIRONMENTS -- each one
|
||||
* takes two environments (the loop's own and the generator's) -- in a single
|
||||
* program. If EXIT abandoned the generator environment instead of releasing
|
||||
* it, this exhausts the pool partway through and the run reports "Environment
|
||||
* pool exhausted" instead of finishing.
|
||||
*/
|
||||
static void test_exit_releases_generator_for_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 FOR EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 NEXT V#\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief The same leak check for DO EACH/EXIT. */
|
||||
static void test_exit_releases_generator_do_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 DO EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 LOOP\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT partway through, with more of the generator left to run, still
|
||||
* frees the environment for the next construct that needs one.
|
||||
*/
|
||||
static void test_exit_partway_through(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH V# IN COUNTUP(10)\n"
|
||||
"70 PRINT V#\n"
|
||||
"80 IF V# = 2 THEN EXIT\n"
|
||||
"90 NEXT V#\n"
|
||||
"100 PRINT \"AFTER\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nAFTER\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Abandoning a generator that is itself suspended inside a FOR EACH
|
||||
* over another generator releases the inner generator too.
|
||||
*
|
||||
* The inner generator's environment hangs off the *loop* scope inside OUTER's
|
||||
* body as a child, off the parent chain -- the one place a bare parent walk
|
||||
* never looks. Before akbasic_runtime_release_generator() recursed into
|
||||
* `forGeneratorEnv`, every trip through this loop stranded one pool slot and
|
||||
* the 13th trip died with "Environment pool exhausted".
|
||||
*/
|
||||
static void test_exit_releases_nested_generators(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN INNER(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 EMIT 2\n"
|
||||
"40 END GEN\n"
|
||||
"50 GEN OUTER(N#)\n"
|
||||
"60 FOR EACH I# IN INNER(0)\n"
|
||||
"70 EMIT I#\n"
|
||||
"80 NEXT I#\n"
|
||||
"90 END GEN\n"
|
||||
"100 FOR K# = 1 TO 40\n"
|
||||
"110 FOR EACH V# IN OUTER(0)\n"
|
||||
"120 EXIT\n"
|
||||
"130 NEXT V#\n"
|
||||
"140 NEXT K#\n"
|
||||
"150 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A LOOP UNTIL that stops a DO EACH early releases the generator it
|
||||
* abandons, every time.
|
||||
*/
|
||||
static void test_loop_condition_releases_generator(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR K# = 1 TO 40\n"
|
||||
"70 DO EACH V# IN COUNTUP(10)\n"
|
||||
"80 LOOP UNTIL V# = 2\n"
|
||||
"90 NEXT K#\n"
|
||||
"100 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RETURN standing in a GEN's own frame ends the generator early,
|
||||
* exactly as END GEN would -- a GEN is a function at heart.
|
||||
*/
|
||||
static void test_return_ends_generator(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN G(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 RETURN\n"
|
||||
"40 EMIT 2\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH V# IN G(0)\n"
|
||||
"70 PRINT V#\n"
|
||||
"80 NEXT V#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\nDONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RETURN with a value inside a GEN is refused: values leave a GEN one
|
||||
* at a time, through EMIT, and there is no return slot waiting.
|
||||
*/
|
||||
static void test_return_value_in_generator_refused(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program_bounded("10 GEN G(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 RETURN 99\n"
|
||||
"40 END GEN\n"
|
||||
"50 FOR EACH V# IN G(0)\n"
|
||||
"60 PRINT V#\n"
|
||||
"70 NEXT V#\n"
|
||||
"80 PRINT \"UNREACHABLE\"\n", 2000));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "1\n") != NULL,
|
||||
"expected the first EMIT in \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||
"RETURN with a value must stop the run, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The unwind primitive releases a popped scope's suspended generator.
|
||||
*
|
||||
* Built by hand rather than through BASIC because the paths that need this --
|
||||
* the error unwinds in pump_generator() and call_function() -- only trigger
|
||||
* on C-level failures a program cannot politely ask for. The shape is the
|
||||
* one EMIT leaves behind: a loop scope holding a detached generator child,
|
||||
* with a further scope active above it.
|
||||
*/
|
||||
static void test_unwind_releases_suspended_generators(void)
|
||||
{
|
||||
akbasic_Environment *root = NULL;
|
||||
akbasic_Environment *loopenv = NULL;
|
||||
akbasic_Environment *genenv = NULL;
|
||||
akbasic_Environment *forenv = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
root = HARNESS_RUNTIME.environment;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
loopenv = HARNESS_RUNTIME.environment;
|
||||
loopenv->isEachLoop = true;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
genenv = HARNESS_RUNTIME.environment;
|
||||
genenv->isGenerator = true;
|
||||
TEST_REQUIRE_OK(akbasic_runtime_detach_environment(&HARNESS_RUNTIME));
|
||||
loopenv->forGeneratorEnv = genenv;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
forenv = HARNESS_RUNTIME.environment;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_unwind_to_environment(&HARNESS_RUNTIME, root));
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
||||
"unwind must land on the target scope");
|
||||
TEST_REQUIRE(!forenv->used && !loopenv->used && !genenv->used,
|
||||
"unwind must release the chain and the suspended generator");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||
* EACH/DO EACH, fails cleanly.
|
||||
*
|
||||
* EMIT requires `isGenerator`, which only a FOR EACH/DO EACH invocation sets
|
||||
* -- an ordinary call pushes a plain environment, exactly as a DEF's does --
|
||||
* so the first EMIT the call reaches is where this is refused.
|
||||
*/
|
||||
static void test_called_like_a_function(void)
|
||||
{
|
||||
akbasic_Value *args[1];
|
||||
akbasic_Value argvalue;
|
||||
akbasic_Value *out = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||||
"10 GEN ONE(N#)\n"
|
||||
"20 EMIT N#\n"
|
||||
"30 END GEN\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_value_zero(&argvalue));
|
||||
argvalue.valuetype = AKBASIC_TYPE_INTEGER;
|
||||
argvalue.intval = 5;
|
||||
args[0] = &argvalue;
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ONE", args, 1, &out));
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief EMIT reached with no enclosing GEN invocation is refused. */
|
||||
static void test_emit_outside_gen(void)
|
||||
{
|
||||
akbasic_ASTLeaf *leaf = NULL;
|
||||
akbasic_Value *out = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||
TEST_REQUIRE_OK(harness_parse("EMIT 5", &leaf));
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A GEN invoking itself, directly, is refused rather than recursing.
|
||||
*
|
||||
* Bounded tightly: a program that recursed forever would hang the whole
|
||||
* suite, and this is exactly the case that must not.
|
||||
*/
|
||||
static void test_self_recursion_refused(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program_bounded("10 GEN RECURSIVE(N#)\n"
|
||||
"20 FOR EACH X# IN RECURSIVE(N# + 1)\n"
|
||||
"30 EMIT X#\n"
|
||||
"40 NEXT X#\n"
|
||||
"50 END GEN\n"
|
||||
"60 PRINT \"BEFORE\"\n"
|
||||
"70 FOR EACH R# IN RECURSIVE(1)\n"
|
||||
"80 PRINT R#\n"
|
||||
"90 NEXT R#\n"
|
||||
"100 PRINT \"UNREACHABLE\"\n", 2000));
|
||||
/* Whatever else happened, the line before the recursive call ran and the
|
||||
one two lines after invoking it -- which would only print after the
|
||||
loop completed successfully -- did not. */
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "BEFORE\n") != NULL,
|
||||
"expected \"BEFORE\" in \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||
"self-recursion must not reach \"UNREACHABLE\", got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Independent (sibling) FOR EACH invocations of the same GEN are not
|
||||
* self-recursion, even nested.
|
||||
*/
|
||||
static void test_sibling_invocations_allowed(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH A# IN COUNTUP(2)\n"
|
||||
"70 FOR EACH B# IN COUNTUP(2)\n"
|
||||
"80 PRINT A# * 10 + B#\n"
|
||||
"90 NEXT B#\n"
|
||||
"100 NEXT A#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_room_objects_smoke();
|
||||
test_exit_releases_generator_for_each();
|
||||
test_exit_releases_generator_do_each();
|
||||
test_exit_partway_through();
|
||||
test_exit_releases_nested_generators();
|
||||
test_loop_condition_releases_generator();
|
||||
test_return_ends_generator();
|
||||
test_return_value_in_generator_refused();
|
||||
test_unwind_releases_suspended_generators();
|
||||
test_called_like_a_function();
|
||||
test_emit_outside_gen();
|
||||
test_self_recursion_refused();
|
||||
test_sibling_invocations_allowed();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
10 REM A GEN invoked like an ordinary function is refused, not silently run.
|
||||
20 GEN ONE(N#)
|
||||
30 EMIT N#
|
||||
40 END GEN
|
||||
50 X# = ONE(5)
|
||||
60 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,2 @@
|
||||
? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
10 REM A DO EACH takes its condition on the LOOP, not on the DO line.
|
||||
20 GEN ONE(N#)
|
||||
30 EMIT N#
|
||||
40 END GEN
|
||||
50 DO EACH V# IN ONE(1) WHILE V# < 9
|
||||
60 PRINT V#
|
||||
70 LOOP
|
||||
80 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,2 @@
|
||||
? 50 : PARSE ERROR DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here
|
||||
|
||||
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
@@ -0,0 +1,14 @@
|
||||
10 REM Same generator as generators_foreach.bas, via DO EACH ... LOOP.
|
||||
20 DIM OBJ#(3)
|
||||
30 OBJ#(0) = 100
|
||||
40 OBJ#(1) = 200
|
||||
50 OBJ#(2) = 300
|
||||
60 GEN ROOMOBJECTS(R#)
|
||||
70 FOR I# = 0 TO 2
|
||||
80 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
90 NEXT I#
|
||||
100 END GEN
|
||||
110 DO EACH O# IN ROOMOBJECTS(0)
|
||||
120 PRINT O#
|
||||
130 LOOP
|
||||
140 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
@@ -0,0 +1,3 @@
|
||||
10 REM EMIT outside any GEN invocation is refused cleanly.
|
||||
20 EMIT 5
|
||||
30 PRINT "UNREACHABLE"
|
||||
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
? 20 : RUNTIME ERROR EMIT outside the context of a GEN body
|
||||
|
||||
11
tests/language/flowcontrol/generators_empty.bas
Normal file
11
tests/language/flowcontrol/generators_empty.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM A GEN with no EMIT at all runs its FOR EACH/DO EACH body zero times.
|
||||
20 GEN NOTHING(N#)
|
||||
30 END GEN
|
||||
40 FOR EACH X# IN NOTHING(0)
|
||||
50 PRINT "NEVER FOR"
|
||||
60 NEXT X#
|
||||
70 PRINT "AFTER FOR"
|
||||
80 DO EACH Y# IN NOTHING(0)
|
||||
90 PRINT "NEVER DO"
|
||||
100 LOOP
|
||||
110 PRINT "AFTER DO"
|
||||
2
tests/language/flowcontrol/generators_empty.txt
Normal file
2
tests/language/flowcontrol/generators_empty.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
AFTER FOR
|
||||
AFTER DO
|
||||
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM The GEN/FOR EACH example from issue #57, with real arrays standing in
|
||||
20 REM for OBJCOUNT%/VISIBLE/OBJ%.
|
||||
30 DIM OBJ#(3)
|
||||
40 OBJ#(0) = 100
|
||||
50 OBJ#(1) = 200
|
||||
60 OBJ#(2) = 300
|
||||
70 GEN ROOMOBJECTS(R#)
|
||||
80 FOR I# = 0 TO 2
|
||||
90 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
100 NEXT I#
|
||||
110 END GEN
|
||||
120 FOR EACH O# IN ROOMOBJECTS(0)
|
||||
130 PRINT O#
|
||||
140 NEXT O#
|
||||
150 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM A WHILE or UNTIL on the LOOP composes with DO EACH: it is checked
|
||||
20 REM after each trip through the body, with the loop variable still
|
||||
30 REM holding that trip's value. Stopping abandons the generator cleanly.
|
||||
40 GEN COUNTUP(N#)
|
||||
50 FOR I# = 1 TO N#
|
||||
60 EMIT I#
|
||||
70 NEXT I#
|
||||
80 END GEN
|
||||
90 DO EACH V# IN COUNTUP(10)
|
||||
100 PRINT V#
|
||||
110 LOOP UNTIL V# = 3
|
||||
120 DO EACH W# IN COUNTUP(4)
|
||||
130 PRINT W# * 10
|
||||
140 LOOP WHILE W# < 3
|
||||
150 PRINT "DONE"
|
||||
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
1
|
||||
2
|
||||
3
|
||||
10
|
||||
20
|
||||
30
|
||||
DONE
|
||||
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
@@ -0,0 +1,10 @@
|
||||
10 REM A stray NEXT with nothing open behaves like the plain-FOR case: the
|
||||
20 REM FOR EACH's own NEXT closes it cleanly, and a second one errors.
|
||||
30 GEN ONE(N#)
|
||||
40 EMIT N#
|
||||
50 END GEN
|
||||
60 FOR EACH O# IN ONE(1)
|
||||
70 PRINT O#
|
||||
80 NEXT O#
|
||||
90 NEXT O#
|
||||
100 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,3 @@
|
||||
1
|
||||
? 90 : RUNTIME ERROR NEXT outside the context of FOR
|
||||
|
||||
22
tests/language/flowcontrol/generators_nested.bas
Normal file
22
tests/language/flowcontrol/generators_nested.bas
Normal file
@@ -0,0 +1,22 @@
|
||||
10 REM Nested FOR EACH/DO EACH over the same GEN with different arguments,
|
||||
20 REM in every combination of the two loop shapes.
|
||||
30 GEN COUNTUP(N#)
|
||||
40 FOR I# = 1 TO N#
|
||||
50 EMIT I#
|
||||
60 NEXT I#
|
||||
70 END GEN
|
||||
80 FOR EACH A# IN COUNTUP(2)
|
||||
90 FOR EACH B# IN COUNTUP(3)
|
||||
100 PRINT A# * 10 + B#
|
||||
110 NEXT B#
|
||||
120 NEXT A#
|
||||
130 DO EACH C# IN COUNTUP(2)
|
||||
140 DO EACH D# IN COUNTUP(2)
|
||||
150 PRINT C# * 100 + D#
|
||||
160 LOOP
|
||||
170 LOOP
|
||||
180 FOR EACH E# IN COUNTUP(2)
|
||||
190 DO EACH F# IN COUNTUP(2)
|
||||
200 PRINT E# * 1000 + F#
|
||||
210 LOOP
|
||||
220 NEXT E#
|
||||
14
tests/language/flowcontrol/generators_nested.txt
Normal file
14
tests/language/flowcontrol/generators_nested.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
11
|
||||
12
|
||||
13
|
||||
21
|
||||
22
|
||||
23
|
||||
101
|
||||
102
|
||||
201
|
||||
202
|
||||
1001
|
||||
1002
|
||||
2001
|
||||
2002
|
||||
15
tests/language/flowcontrol/generators_return.bas
Normal file
15
tests/language/flowcontrol/generators_return.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM RETURN ends a GEN early, exactly as END GEN would: a GEN is a
|
||||
20 REM function at heart, and only EMIT is different about it. Like a
|
||||
30 REM GOSUB's or DEF's RETURN, it must stand in the GEN's own scope,
|
||||
40 REM not inside a FOR or DO the body opened.
|
||||
50 GEN FIRSTFEW(N#)
|
||||
60 EMIT 1
|
||||
70 IF N# < 2 THEN RETURN
|
||||
80 EMIT 2
|
||||
90 IF N# < 3 THEN RETURN
|
||||
100 EMIT 3
|
||||
110 END GEN
|
||||
120 FOR EACH V# IN FIRSTFEW(2)
|
||||
130 PRINT V#
|
||||
140 NEXT V#
|
||||
150 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_return.txt
Normal file
3
tests/language/flowcontrol/generators_return.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
1
|
||||
2
|
||||
DONE
|
||||
11
tests/language/flowcontrol/generators_string.bas
Normal file
11
tests/language/flowcontrol/generators_string.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM FOR EACH/DO EACH accept any emitted type, not just numeric.
|
||||
20 GEN WORDS(N#)
|
||||
30 EMIT "HELLO"
|
||||
40 EMIT "WORLD"
|
||||
50 END GEN
|
||||
60 FOR EACH W$ IN WORDS(0)
|
||||
70 PRINT W$
|
||||
80 NEXT W$
|
||||
90 DO EACH V$ IN WORDS(0)
|
||||
100 PRINT V$
|
||||
110 LOOP
|
||||
4
tests/language/flowcontrol/generators_string.txt
Normal file
4
tests/language/flowcontrol/generators_string.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
HELLO
|
||||
WORLD
|
||||
HELLO
|
||||
WORLD
|
||||
3
tests/language/functions/asc.bas
Normal file
3
tests/language/functions/asc.bas
Normal file
@@ -0,0 +1,3 @@
|
||||
10 PRINT "97 : " + ASC("a")
|
||||
20 PRINT "65 : " + ASC("A")
|
||||
30 PRINT "64 : " + ASC("@")
|
||||
3
tests/language/functions/asc.txt
Normal file
3
tests/language/functions/asc.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
97 : 97
|
||||
65 : 65
|
||||
64 : 64
|
||||
@@ -162,6 +162,26 @@ int main(void)
|
||||
expect_int("A# = INSTR(\"HELLO\", \"LL\")", 2);
|
||||
expect_int("A# = INSTR(\"HELLO\", \"ZZ\")", -1);
|
||||
|
||||
/* RND auto-seeds from host time and follows the documented LCG. */
|
||||
HARNESS_RUNTIME.rndseeded = false;
|
||||
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 12345));
|
||||
expect_int("A# = RND(6)", 0);
|
||||
expect_int("A# = RND(6)", 4);
|
||||
expect_int("A# = RND(6)", 1);
|
||||
expect_int("A# = RND(6)", 0);
|
||||
expect_int("A# = RND(6)", 1);
|
||||
expect_int("A# = RND(1)", 0);
|
||||
TEST_REQUIRE_STATUS(eval_line("A# = RND(0)", &out), AKBASIC_ERR_VALUE);
|
||||
TEST_REQUIRE_STATUS(eval_line("A# = RND(-1)", &out), AKBASIC_ERR_VALUE);
|
||||
TEST_REQUIRE_STATUS(eval_line("A# = RND(\"x\")", &out), AKBASIC_ERR_TYPE);
|
||||
|
||||
/* ASC is the inverse of CHR for ASCII and non-ASCII code points. */
|
||||
expect_int("A# = ASC(CHR(97))", 97);
|
||||
expect_int("A# = ASC(\"A\")", 65);
|
||||
expect_int("A# = ASC(CHR(8364))", 8364);
|
||||
TEST_REQUIRE_STATUS(eval_line("A# = ASC(\"\")", &out), AKBASIC_ERR_BOUNDS);
|
||||
TEST_REQUIRE_STATUS(eval_line("A# = ASC(65)", &out), AKBASIC_ERR_TYPE);
|
||||
|
||||
/* An unknown verb is diagnosed rather than silently ignored. */
|
||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, NULL, &out),
|
||||
|
||||
@@ -183,6 +183,33 @@ static void test_structure_parameters(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pointer parameters do not consume value-pool slots per call.
|
||||
*
|
||||
* The pointer's parameter variable owns only one reference to the caller's
|
||||
* record, so that reference can live in the variable's inline slot. The target
|
||||
* remains in the caller's storage. One pointer parameter and 8,000 calls make
|
||||
* one leaked slot per call fail against the 4,096-slot value pool.
|
||||
*/
|
||||
static void test_pointer_parameters_do_not_leak_value_slots(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program_bounded("10 TYPE CRATE\n"
|
||||
"20 W#\n"
|
||||
"30 END TYPE\n"
|
||||
"40 DIM A@ AS CRATE\n"
|
||||
"50 DIM P@ AS PTR TO CRATE\n"
|
||||
"60 POINT P@ AT A@\n"
|
||||
"70 DEF POKEIT(P@ AS PTR TO CRATE)\n"
|
||||
"80 P@->W# = P@->W# + 1\n"
|
||||
"90 RETURN P@->W#\n"
|
||||
"100 FOR I# = 1 TO 8000\n"
|
||||
"110 R# = POKEIT(P@)\n"
|
||||
"120 NEXT I#\n"
|
||||
"130 PRINT A@.W#\n", 1000000));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "8000\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A structure parameter must name its type, and the type is checked.
|
||||
*
|
||||
@@ -454,6 +481,7 @@ int main(void)
|
||||
test_runaway_recursion_is_diagnosed();
|
||||
test_single_expression_form();
|
||||
test_structure_parameters();
|
||||
test_pointer_parameters_do_not_leak_value_slots();
|
||||
test_structure_parameter_types_are_checked();
|
||||
test_call_scopes_are_reclaimed();
|
||||
test_calls_do_not_leak_value_slots();
|
||||
|
||||
@@ -66,15 +66,18 @@ int main(void)
|
||||
/*
|
||||
* 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. Any other missing
|
||||
* handler is a verb that would report "Unknown command" at runtime.
|
||||
* 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 ||
|
||||
|
||||
Reference in New Issue
Block a user