/** * @file unnumbered.c * @brief Programs loaded without line numbers. * * A script written against `LABEL` and `GOTO NAME` never names a line number, so * the numbers it used to be forced to carry were decoration. A loaded line that * arrives without one now gets the slot after the last line filed, which is the * rule `akbasic_runtime_file_line()` holds for all three loading paths. * * What is asserted here is the seam, not the language: which slot a line lands * in, that both spellings execute identically, that the prompt's rule is * untouched, and that a collision is refused rather than losing a line. */ #include #include #include #include #include "harness.h" #include "testutil.h" /** @brief Load a program from a string and run it to completion. */ 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 Read a program through the sink, the way the file driver does. */ static akerr_ErrorContext AKERR_NOIGNORE *runstream_program(const char *source) { PREPARE_ERROR(errctx); PASS(errctx, harness_start(source)); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0)); SUCCEED_RETURN(errctx); } /** @brief A program with no numbers at all runs top to bottom. */ static void test_runs_in_order(void) { TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n" "PRINT \"TWO\"\n" "PRINT \"THREE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\nTHREE\n"); /* Slot 0 is left empty and the program starts at 1. */ TEST_REQUIRE_STR(HARNESS_RUNTIME.source[0].code, ""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[3].code, "PRINT \"THREE\""); /* And every one of them knows it was assigned rather than written. */ TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 should not be marked numbered"); TEST_REQUIRE(!HARNESS_RUNTIME.source[3].numbered, "line 3 should not be marked numbered"); harness_stop(); } /** * @brief Two unnumbered lines in a row are two lines. * * This is the defect the feature replaces, stated on its own: the second used to * be filed under the cursor unchanged, on top of the first, with no error and no * output. A test that only checked the *last* line would have passed throughout. */ static void test_second_line_does_not_replace_the_first(void) { TEST_REQUIRE_OK(run_program("PRINT \"FIRST\"\n" "PRINT \"SECOND\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n"); harness_stop(); } /** @brief A blank line is whitespace, and costs no slot. */ static void test_blank_lines_cost_nothing(void) { TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n" "\n" "\n" "PRINT \"TWO\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n"); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\""); harness_stop(); } /** * @brief A numbered line moves the cursor; unnumbered ones follow from there. * * Execution order is slot order, not file order, which is the whole reason the * two can be mixed at all: `50 PRINT "C"` runs before `100 PRINT "A"` however * they were written down. */ static void test_mixed_numbering(void) { TEST_REQUIRE_OK(run_program("100 PRINT \"A\"\n" "PRINT \"B\"\n" "50 PRINT \"C\"\n" "PRINT \"D\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "C\nD\nA\nB\n"); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[50].code, "PRINT \"C\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[51].code, "PRINT \"D\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "PRINT \"A\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[101].code, "PRINT \"B\""); /* The written numbers are marked as such; the two that followed are not. */ TEST_REQUIRE(HARNESS_RUNTIME.source[50].numbered, "line 50 was written and should be marked"); TEST_REQUIRE(HARNESS_RUNTIME.source[100].numbered, "line 100 was written and should be marked"); TEST_REQUIRE(!HARNESS_RUNTIME.source[51].numbered, "line 51 was assigned, not written"); TEST_REQUIRE(!HARNESS_RUNTIME.source[101].numbered, "line 101 was assigned, not written"); harness_stop(); } /** @brief Branching by label needs no numbers anywhere. */ static void test_label_branching(void) { TEST_REQUIRE_OK(run_program("GOSUB GREET\n" "GOTO DONE\n" "PRINT \"NOT REACHED\"\n" "LABEL GREET\n" "PRINT \"HELLO\"\n" "RETURN\n" "LABEL DONE\n" "PRINT \"BYE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\nBYE\n"); harness_stop(); } /** * @brief A collision involving an assigned number is refused, not overwritten. * * `1 PRINT "C"` names a slot two unnumbered lines have already been given. Losing * one of them to that is not recoverable, so the load fails instead. */ static void test_collision_is_refused(void) { TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME, "PRINT \"A\"\n" "PRINT \"B\"\n" "1 PRINT \"C\"\n"), AKBASIC_ERR_BOUNDS); harness_stop(); /* The mirror image: an assigned line landing on one already written. */ TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME, "2 PRINT \"A\"\n" "1 PRINT \"B\"\n" "PRINT \"C\"\n"), AKBASIC_ERR_BOUNDS); harness_stop(); } /** @brief Two lines carrying the same written number still keep the last. */ static void test_duplicate_written_numbers_still_replace(void) { TEST_REQUIRE_OK(run_program("10 PRINT \"OLD\"\n" "10 PRINT \"NEW\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "NEW\n"); harness_stop(); } /** @brief The same program through the sink lands in the same slots. */ static void test_runstream_assigns_the_same_slots(void) { TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n" "PRINT \"TWO\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n"); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\""); harness_stop(); } /** * @brief A file ending in a blank line keeps its last line. * * `tests/reference/language/arithmetic/integer.bas` is this shape, and both this * interpreter and the reference used to erase the last line before running it -- * the blank line was filed under the cursor, which for a line with no number of * its own is the number of the line before it. */ static void test_trailing_blank_line_keeps_the_last_line(void) { TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n" "PRINT \"LAST\"\n" "\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nLAST\n"); harness_stop(); } /** * @brief The prompt still tells a program line from a statement by its number. * * The one rule the feature does not touch. `PRINT 2 + 2` typed at a prompt must * answer 4 rather than quietly becoming line 1 of a program. */ static void test_prompt_still_runs_direct_mode(void) { TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n10 PRINT \"FILED\"\nLIST\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); /* 4 came out now; the numbered line went into the program and LIST found it. */ TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL, "PRINT 2 + 2 at the prompt should have answered 4, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "10 PRINT \"FILED\"") != NULL, "the numbered line should have been filed and listed, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, ""); harness_stop(); } /** * @brief Branching by number to a line nobody numbered is refused before the run. * * `GOTO 2` in a program with no line numbers finds the second line and branches * there. The prescan says so instead, and says it before any line executes -- so * the program produces the error and nothing else, not the error partway through * its output. */ static void test_numeric_branch_to_an_assigned_line_is_refused(void) { TEST_REQUIRE_OK(run_program("PRINT \"A\"\n" "PRINT \"B\"\n" "GOTO 2\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "did not number") != NULL, "expected a refusal naming the unnumbered target, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "A\n") == NULL, "the program should not have run at all, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* The refusal names the offending line, not wherever the loader stopped. */ TEST_REQUIRE_OK(run_program("GOTO 4\n" "PRINT \"B\"\n" "PRINT \"C\"\n" "PRINT \"D\"\n" "PRINT \"E\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 1 :") != NULL, "expected the error to name line 1, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief A written number is still a legal target, in a mixed program too. */ static void test_numeric_branch_to_a_written_line_is_allowed(void) { TEST_REQUIRE_OK(run_program("10 PRINT \"A\"\n" "20 IF A# = 0 THEN A# = 1 : GOTO 10\n" "30 PRINT \"OK\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nA\nOK\n"); harness_stop(); /* Mixed: line 10 was written, so a branch to it stands. */ TEST_REQUIRE_OK(run_program("10 PRINT \"A\"\n" "PRINT \"B\"\n" "20 IF A# = 0 THEN A# = 1 : GOTO 10\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nB\nA\nB\n"); harness_stop(); } /** @brief A branch to an empty line is left alone, as RENUMBER leaves it alone. */ static void test_numeric_branch_to_an_empty_line_is_allowed(void) { TEST_REQUIRE_OK(run_program("PRINT \"A\"\n" "GOTO 500\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\n"); harness_stop(); } /** @brief A label target never reaches the check, so it cannot be refused by it. */ static void test_label_targets_are_never_refused(void) { TEST_REQUIRE_OK(run_program("GOTO DONE\n" "PRINT \"SKIPPED\"\n" "LABEL DONE\n" "PRINT \"OK\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n"); harness_stop(); } /** @brief A number inside a string literal is not a branch target. */ static void test_a_number_in_a_string_is_not_a_target(void) { TEST_REQUIRE_OK(run_program("PRINT \"A\"\n" "PRINT \"GOTO 2\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nGOTO 2\n"); harness_stop(); } /** * @brief RENUMBER makes a numberless program branchable by number again. * * It moves line 3 to line 30 *and* rewrites `GOTO 3` to `GOTO 30`, and every line * it touches comes out numbered -- so the branch the check refused a moment ago * is now naming a number the program owns. That is the remedy the refusal names. */ /** * @brief A name that merely *contains* a branch verb is not a branch. * * The target prescan walks a line character by character looking for `GOTO`, * `GOSUB`, `COLLISION` and the rest, and it checked only the character *after* a * match. So `RCOLLISION(1, 1)` found `COLLISION` at its second character, read * the `(1,` that followed as a handler line number, and refused the whole * program with "branch to line 1, which the program did not number" -- naming a * line containing no branch at all. * * `RCOLLISION` is the first name in the language to reach it. The general case * is any identifier ending in a branch verb, which is why the second half of * this test does not mention collision at all. */ static void test_verb_inside_a_name_is_not_a_branch(void) { TEST_REQUIRE_OK(run_program("X# = 0\n" "T# = RCOLLISION(1, 1)\n" "PRINT \"RAN \" + T#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "RAN 0\n"); harness_stop(); /* And the general shape: a variable whose name ends in a branch verb. */ TEST_REQUIRE_OK(run_program("MYGOTO# = 7\n" "PRINT MYGOTO#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n"); harness_stop(); /* The trailing check still works: a name *starting* with one is not a branch. */ TEST_REQUIRE_OK(run_program("GOTOX# = 9\n" "PRINT GOTOX#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "9\n"); harness_stop(); } static void test_renumber_makes_numeric_branches_legal(void) { TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "PRINT \"A\"\n" "GOTO 3\n" "PRINT \"B\"\n")); /* Before: line 3 was assigned a number, so branching to it is refused. */ TEST_REQUIRE_STATUS(akbasic_runtime_check_targets(&HARNESS_RUNTIME), AKBASIC_ERR_SYNTAX); TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0)); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "GOTO 30"); TEST_REQUIRE_OK(akbasic_runtime_check_targets(&HARNESS_RUNTIME)); harness_stop(); } /** @brief RENUMBER numbers everything, which is what asking for it means. */ static void test_renumber_marks_every_line_numbered(void) { TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "PRINT \"ONE\"\n" "PRINT \"TWO\"\n")); TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 starts out assigned"); TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0)); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\""); TEST_REQUIRE(HARNESS_RUNTIME.source[10].numbered, "RENUMBER should mark line 10 numbered"); TEST_REQUIRE(HARNESS_RUNTIME.source[20].numbered, "RENUMBER should mark line 20 numbered"); harness_stop(); } int main(void) { test_runs_in_order(); test_second_line_does_not_replace_the_first(); test_blank_lines_cost_nothing(); test_mixed_numbering(); test_label_branching(); test_collision_is_refused(); test_duplicate_written_numbers_still_replace(); test_runstream_assigns_the_same_slots(); test_trailing_blank_line_keeps_the_last_line(); test_prompt_still_runs_direct_mode(); test_numeric_branch_to_an_assigned_line_is_refused(); test_numeric_branch_to_a_written_line_is_allowed(); test_numeric_branch_to_an_empty_line_is_allowed(); test_label_targets_are_never_refused(); test_a_number_in_a_string_is_not_a_target(); test_verb_inside_a_name_is_not_a_branch(); test_renumber_makes_numeric_branches_legal(); test_renumber_marks_every_line_numbered(); return akbasic_test_failures; }