Refuse a branch to a line the program did not number
GOTO 100 in a script written without line numbers finds the hundredth line and branches there. Silent, plausible and wrong: the test for it loops forever printing the second line when the check is removed. akbasic_runtime_check_targets() is a fourth prescan beside the label, DATA and TYPE ones, run on every entry into MODE_RUN -- the earliest the check can be made and the only place all four ways a program arrives pass through. A target naming an empty line is still allowed, for the same reason RENUMBER leaves one alone, and a fully numbered program is unaffected, which is every program that existed before this. It shares RENUMBER's walk rather than repeating it. renumber.c grows an akbasic_TargetWalk -- a self pointer and a visit function -- and rewrite_line() takes one. RENUMBER's visitor substitutes the number a line moved to; the check's substitutes the number unchanged and raises. One walk, so the two cannot disagree about what a branch target is. The check points environment->lineno at the line being walked so the "? N :" prefix names the offending line. The other three prescans do not and report whichever line the loader stopped on; TODO.md section 5 item 64 records it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,105 @@ static void test_prompt_still_runs_direct_mode(void)
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
{
|
||||
@@ -246,6 +345,12 @@ int main(void)
|
||||
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_renumber_makes_numeric_branches_legal();
|
||||
test_renumber_marks_every_line_numbered();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user