Files
akbasic/tests/unnumbered.c

393 lines
14 KiB
C
Raw Permalink Normal View History

Give a loaded line a number when it arrives without one A script written against LABEL and GOTO NAME never names a line number, so the numbers it had to carry were decoration. akbasic_runtime_load(), RUNSTREAM and DLOAD now file an unnumbered line one slot after the last one filed; a numbered line is filed under its number and moves the cursor, so the two mix. akbasic_runtime_file_line() is the one implementation of that rule, so the three paths cannot drift. The prompt is untouched. A line typed without a number is still direct mode and still runs now -- that is the only thing separating program text from a statement at a REPL, and it is why this is a loading feature. What this replaces was silent data loss: an unnumbered line was filed under the cursor unchanged, on top of the line before it. A blank line therefore erased whatever preceded it, and RUNSTREAM did not skip blank lines the way the other two paths did. That moves one golden file, and the reference had the same defect. language/arithmetic/integer.bas has four PRINT statements, an expectation with three values, and a trailing blank line that erased 40 PRINT 4 - 2 before the program ran. The expectation is now 4 4 2 2. tests/reference/README.md records the divergence and TODO.md section 5 item 63 says why. akbasic_SourceLine grows a `numbered` flag so an assigned number can be told from a written one. RENUMBER sets it on every line it touches; NEW, DELETE and DLOAD clear it. hadlinenumber moves to akbasic_scanner_scan(), so it always describes the line just scanned rather than only the REPL's. Two lines carrying the same written number still keep the last, as they always have. That is a separate decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:33:27 -04:00
/**
* @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.
*/
Give the characters breakout its bricks as collision geometry `HITTEST`, `XBRICK` and `YBRICK` are gone -- about forty lines that divided pixels by cell sizes to recover a grid index, tested the ball's leading edge rather than its box, and could miss a brick clipped at the corner by seven pixels' worth of ball. In their place: sixty `SOLID` rectangles registered as the wall is built, `COLLISION 2, BRICKHIT`, and a fifteen-line handler. The handler reads better than what it replaces because it asks rather than derives. `RCOLLISION(1, 1)` is which brick -- the id is the array index plus one, so nothing is looked up. Fields 2, 3 and 4 are the way out and how far, so the ball is pushed exactly clear instead of being restored to a remembered `OX#`/`OY#`. Field 7 is which axis to reverse, which `TESTCELL` in the other game computes by hand from an overlap rectangle. `MOVEBAL` loses its two brick calls and its position backup. `KILLBR` retires the rectangle in the same breath as clearing the array element, so the next frame cannot hit a brick that is no longer drawn. **A latent defect in the target prescan had to be fixed first, and `RCOLLISION` is the first name in the language to reach it.** `src/renumber.c` walks a line character by character looking for `GOTO`, `GOSUB`, `COLLISION` and the rest, and 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 that contains no branch at all. It now requires a word boundary on both sides. The comment there was already right that the trailing check protects `GOTOX#`; nothing protected `XGOTO#`. `tests/unnumbered.c` covers all three shapes and TODO.md section 6 item 42 records it. The game runs ninety seconds headless with no error line and the attract mode scores 1320, so bricks are being found and broken through the new path. Chapter 17 is not updated yet -- that is the other half of section 6 item 39, and it is a bigger edit than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 11:17:34 -04:00
/**
* @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();
}
Give a loaded line a number when it arrives without one A script written against LABEL and GOTO NAME never names a line number, so the numbers it had to carry were decoration. akbasic_runtime_load(), RUNSTREAM and DLOAD now file an unnumbered line one slot after the last one filed; a numbered line is filed under its number and moves the cursor, so the two mix. akbasic_runtime_file_line() is the one implementation of that rule, so the three paths cannot drift. The prompt is untouched. A line typed without a number is still direct mode and still runs now -- that is the only thing separating program text from a statement at a REPL, and it is why this is a loading feature. What this replaces was silent data loss: an unnumbered line was filed under the cursor unchanged, on top of the line before it. A blank line therefore erased whatever preceded it, and RUNSTREAM did not skip blank lines the way the other two paths did. That moves one golden file, and the reference had the same defect. language/arithmetic/integer.bas has four PRINT statements, an expectation with three values, and a trailing blank line that erased 40 PRINT 4 - 2 before the program ran. The expectation is now 4 4 2 2. tests/reference/README.md records the divergence and TODO.md section 5 item 63 says why. akbasic_SourceLine grows a `numbered` flag so an assigned number can be told from a written one. RENUMBER sets it on every line it touches; NEW, DELETE and DLOAD clear it. hadlinenumber moves to akbasic_scanner_scan(), so it always describes the line just scanned rather than only the REPL's. Two lines carrying the same written number still keep the last, as they always have. That is a separate decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:33:27 -04:00
/** @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();
Give the characters breakout its bricks as collision geometry `HITTEST`, `XBRICK` and `YBRICK` are gone -- about forty lines that divided pixels by cell sizes to recover a grid index, tested the ball's leading edge rather than its box, and could miss a brick clipped at the corner by seven pixels' worth of ball. In their place: sixty `SOLID` rectangles registered as the wall is built, `COLLISION 2, BRICKHIT`, and a fifteen-line handler. The handler reads better than what it replaces because it asks rather than derives. `RCOLLISION(1, 1)` is which brick -- the id is the array index plus one, so nothing is looked up. Fields 2, 3 and 4 are the way out and how far, so the ball is pushed exactly clear instead of being restored to a remembered `OX#`/`OY#`. Field 7 is which axis to reverse, which `TESTCELL` in the other game computes by hand from an overlap rectangle. `MOVEBAL` loses its two brick calls and its position backup. `KILLBR` retires the rectangle in the same breath as clearing the array element, so the next frame cannot hit a brick that is no longer drawn. **A latent defect in the target prescan had to be fixed first, and `RCOLLISION` is the first name in the language to reach it.** `src/renumber.c` walks a line character by character looking for `GOTO`, `GOSUB`, `COLLISION` and the rest, and 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 that contains no branch at all. It now requires a word boundary on both sides. The comment there was already right that the trailing check protects `GOTOX#`; nothing protected `XGOTO#`. `tests/unnumbered.c` covers all three shapes and TODO.md section 6 item 42 records it. The game runs ninety seconds headless with no error line and the attract mode scores 1320, so bricks are being found and broken through the new path. Chapter 17 is not updated yet -- that is the other half of section 6 item 39, and it is a bigger edit than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 11:17:34 -04:00
test_verb_inside_a_name_is_not_a_branch();
test_renumber_makes_numeric_branches_legal();
Give a loaded line a number when it arrives without one A script written against LABEL and GOTO NAME never names a line number, so the numbers it had to carry were decoration. akbasic_runtime_load(), RUNSTREAM and DLOAD now file an unnumbered line one slot after the last one filed; a numbered line is filed under its number and moves the cursor, so the two mix. akbasic_runtime_file_line() is the one implementation of that rule, so the three paths cannot drift. The prompt is untouched. A line typed without a number is still direct mode and still runs now -- that is the only thing separating program text from a statement at a REPL, and it is why this is a loading feature. What this replaces was silent data loss: an unnumbered line was filed under the cursor unchanged, on top of the line before it. A blank line therefore erased whatever preceded it, and RUNSTREAM did not skip blank lines the way the other two paths did. That moves one golden file, and the reference had the same defect. language/arithmetic/integer.bas has four PRINT statements, an expectation with three values, and a trailing blank line that erased 40 PRINT 4 - 2 before the program ran. The expectation is now 4 4 2 2. tests/reference/README.md records the divergence and TODO.md section 5 item 63 says why. akbasic_SourceLine grows a `numbered` flag so an assigned number can be told from a written one. RENUMBER sets it on every line it touches; NEW, DELETE and DLOAD clear it. hadlinenumber moves to akbasic_scanner_scan(), so it always describes the line just scanned rather than only the REPL's. Two lines carrying the same written number still keep the last, as they always have. That is a separate decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:33:27 -04:00
test_renumber_marks_every_line_numbered();
return akbasic_test_failures;
}