/** * @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 #include #include #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; }