194 lines
6.3 KiB
C
194 lines
6.3 KiB
C
|
|
/**
|
||
|
|
* @file for_next.c
|
||
|
|
* @brief Full coverage of FOR/NEXT, which TODO.md section 6 item 4 is gated on.
|
||
|
|
*
|
||
|
|
* That item is `math_plus` mutating its left operand in place when the operand
|
||
|
|
* is mutable, where every other operator clones. It is a real defect -- `A# + 1`
|
||
|
|
* modifying `A#` depending on where the value came from -- and it was left alone
|
||
|
|
* because `NEXT`'s loop increment *relied* on the mutation: fixing one without
|
||
|
|
* the other silently breaks every FOR loop in the language.
|
||
|
|
*
|
||
|
|
* The golden corpus covers a plain loop, a nested pair and the
|
||
|
|
* `waitingForCommand` case. What it does not cover is `STEP`, a negative step, a
|
||
|
|
* float counter, `EXIT`, or a body that assigns to the loop variable -- and
|
||
|
|
* those are the cases where an increment that writes to the wrong storage shows
|
||
|
|
* up. This file is what made the fix safe to make.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#include "harness.h"
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
/** @brief Run a program to completion in RUN mode, from a string. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||
|
|
{
|
||
|
|
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, 0));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief The counter advances by one and stops *after* the limit, not on it. */
|
||
|
|
static void test_plain_loop(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 4\n"
|
||
|
|
"20 PRINT I#\n"
|
||
|
|
"30 NEXT I#\n"));
|
||
|
|
/*
|
||
|
|
* Every value from the start to the limit, once each. That the body sees the
|
||
|
|
* *incremented* value at all is what says the increment reached the variable
|
||
|
|
* rather than a scratch copy of it -- the whole point of this file.
|
||
|
|
*
|
||
|
|
* What the counter reads after the loop is a separate question, and this
|
||
|
|
* interpreter answers it differently from a C128: see tests/for_semantics.c.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n3\n4\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief STEP advances by what it says, and the limit is still a limit. */
|
||
|
|
static void test_step(void)
|
||
|
|
{
|
||
|
|
/*
|
||
|
|
* A step that lands exactly on the limit. The overshooting case is a known
|
||
|
|
* defect and lives in tests/for_semantics.c; what is asserted here is only
|
||
|
|
* that STEP is read and applied.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 0 TO 9 STEP 3\n"
|
||
|
|
"20 PRINT I#\n"
|
||
|
|
"30 NEXT I#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n3\n6\n9\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A negative step counts down, and the comparison flips with it. */
|
||
|
|
static void test_negative_step(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 5 TO 1 STEP -2\n"
|
||
|
|
"20 PRINT I#\n"
|
||
|
|
"30 NEXT I#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n3\n1\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A float counter accumulates in the float field, not the integer one. */
|
||
|
|
static void test_float_counter(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I% = 1.0 TO 2.0 STEP 0.5\n"
|
||
|
|
"20 PRINT I%\n"
|
||
|
|
"30 NEXT I%\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1.000000\n1.500000\n2.000000\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A body that assigns to the loop variable changes where the loop goes.
|
||
|
|
*
|
||
|
|
* The counter is ordinary storage, so writing to it is legal and the loop reads
|
||
|
|
* the written value. This is the case that tells an increment landing in the
|
||
|
|
* variable apart from one landing in a scratch value that happens to be read
|
||
|
|
* back: with a scratch increment the assignment on line 20 would be overwritten
|
||
|
|
* rather than built on.
|
||
|
|
*/
|
||
|
|
static void test_body_assigns_to_counter(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
|
||
|
|
"20 I# = I# + 3\n"
|
||
|
|
"30 PRINT I#\n"
|
||
|
|
"40 NEXT I#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "4\n8\n12\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief EXIT leaves the loop and lands after its NEXT, with the wait cleared. */
|
||
|
|
static void test_exit(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
|
||
|
|
"20 PRINT I#\n"
|
||
|
|
"30 IF I# = 2 THEN EXIT\n"
|
||
|
|
"40 NEXT I#\n"
|
||
|
|
"50 PRINT \"OUT\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nOUT\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* And a second loop afterwards still runs, so the wait really was cleared. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
|
||
|
|
"20 IF I# = 1 THEN EXIT\n"
|
||
|
|
"30 NEXT I#\n"
|
||
|
|
"40 FOR J# = 1 TO 2\n"
|
||
|
|
"50 PRINT J#\n"
|
||
|
|
"60 NEXT J#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Nested loops each advance their own counter and unwind in order. */
|
||
|
|
static void test_nested(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 2\n"
|
||
|
|
"20 FOR J# = 1 TO 2\n"
|
||
|
|
"30 PRINT I# * 10 + J#\n"
|
||
|
|
"40 NEXT J#\n"
|
||
|
|
"50 NEXT I#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief `A# + 1` does not modify `A#`.
|
||
|
|
*
|
||
|
|
* The defect itself, stated as a program. Addition on a variable read out of the
|
||
|
|
* environment used to update the variable in place, so this printed `2` and then
|
||
|
|
* `2` -- the first PRINT changing what the second one saw.
|
||
|
|
*/
|
||
|
|
static void test_addition_does_not_mutate(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 A# = 1\n"
|
||
|
|
"20 PRINT A# + 1\n"
|
||
|
|
"30 PRINT A#\n"
|
||
|
|
"40 PRINT A# + 1\n"
|
||
|
|
"50 PRINT A#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n1\n2\n1\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* The same for a float, and for a string, which concatenates. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 A% = 1.5\n"
|
||
|
|
"20 PRINT A% + 1.0\n"
|
||
|
|
"30 PRINT A%\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2.500000\n1.500000\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 A$ = \"X\"\n"
|
||
|
|
"20 PRINT A$ + \"Y\"\n"
|
||
|
|
"30 PRINT A$\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "XY\nX\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* And inside an expression used twice on one line. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 A# = 5\n"
|
||
|
|
"20 PRINT (A# + 1) + (A# + 1)\n"
|
||
|
|
"30 PRINT A#\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "12\n5\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
test_plain_loop();
|
||
|
|
test_step();
|
||
|
|
test_negative_step();
|
||
|
|
test_float_counter();
|
||
|
|
test_body_assigns_to_counter();
|
||
|
|
test_exit();
|
||
|
|
test_nested();
|
||
|
|
test_addition_does_not_mutate();
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|