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>
357 lines
13 KiB
C
357 lines
13 KiB
C
/**
|
|
* @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 <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#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.
|
|
*/
|
|
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_renumber_makes_numeric_branches_legal();
|
|
test_renumber_marks_every_line_numbered();
|
|
return akbasic_test_failures;
|
|
}
|