diff --git a/CMakeLists.txt b/CMakeLists.txt index 6837184..bbdbed7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,7 @@ set(AKBASIC_TESTS struct_types trap_verbs symtab + unnumbered user_functions value_arithmetic value_bitwise diff --git a/TODO.md b/TODO.md index af030a3..1c02694 100644 --- a/TODO.md +++ b/TODO.md @@ -1355,6 +1355,36 @@ deviations from the reference's *program*: `main.go` and the SDL half of it had not been used for, and both were filed rather than worked around silently. One came back fixed. +63. **Line numbers are optional in a loaded program, and a blank line is not a program line.** + **This one moved a golden file.** A line with no number used to be filed under the loader's + cursor unchanged — that is, on top of the line before it — so two unnumbered lines in a row + silently lost the first, and a *blank* line erased whatever preceded it. The reference does + the same, and `tests/reference/language/arithmetic/integer.bas` is the proof: 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` and + `tests/reference/README.md` records it. + + In its place: `akbasic_runtime_file_line()` (`src/runtime.c`) is the one implementation of + the rule, shared by `akbasic_runtime_load()`, RUNSTREAM and `DLOAD`. A numbered line is + filed under its number and moves the cursor; an unnumbered one takes the slot after it; + blank lines are skipped by all three. A collision involving an assigned number is refused + with `AKBASIC_ERR_BOUNDS` rather than overwriting. `akbasic_SourceLine` grew a `numbered` + flag so deviation 64 can tell the two apart. + + **The prompt is deliberately untouched.** A line typed without a number is direct mode and + 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 rather than a language-wide one. `AUTO` remains the way + to enter program text without typing numbers. + + Consequence worth stating: an unnumbered program is capped at 9998 lines, the cap that + already existed, and its assigned numbers are increment-1 — so `LIST` and `DSAVE` show a + listing with no gaps to insert into. `RENUMBER` before `DSAVE` gives the gaps back, and + marks every line numbered. + + **Not changed:** two lines that both carry the *same* written number still silently keep + the last, as they always have. That is a separate decision with its own corpus risk and it + is not this one. + --- --- diff --git a/include/akbasic/runtime.h b/include/akbasic/runtime.h index e5cf6dd..fadd39f 100644 --- a/include/akbasic/runtime.h +++ b/include/akbasic/runtime.h @@ -45,10 +45,20 @@ typedef enum AKBASIC_ERRCLASS_RUNTIME } akbasic_ErrorClass; +/** + * @brief One stored program line. + * + * `numbered` records whether the program itself wrote this line's number or the + * loader assigned one because the line arrived without. Both are real lines and + * both execute identically; the difference matters only to + * akbasic_runtime_check_targets(), which refuses `GOTO 100` when line 100 is a + * number nobody wrote. + */ typedef struct { char code[AKBASIC_MAX_LINE_LENGTH]; int64_t lineno; + bool numbered; } akbasic_SourceLine; /** @@ -665,15 +675,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_start(akbasic_Runtime *obj, i * its source text. * * Lines are separated by `\n`; a `\r` before it is tolerated. Blank lines are - * skipped. A line with no line number is filed under the last one seen, matching - * what the scanner does in RUNSTREAM mode. This does not run anything: follow it - * with akbasic_runtime_start(obj, AKBASIC_MODE_RUN). + * skipped. + * + * **Line numbers are optional here.** A line that carries one is filed under it; + * a line that does not is filed one slot after the last line filed, so a script + * written entirely with `LABEL` and `GOTO NAME` needs no numbers at all. The two + * can be mixed: a numbered line moves the cursor, and the unnumbered lines after + * it continue from there. This is the rule RUNSTREAM and `DLOAD` follow too; only + * the prompt still requires a number, because that is the one place a number is + * what separates program text from a statement to run now. + * + * This does not run anything: follow it with + * akbasic_runtime_start(obj, AKBASIC_MODE_RUN). * * @param obj Object to initialize, inspect, or modify. * @param source Whole program text; must not be NULL. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When `obj` or `source` is NULL. - * @throws AKBASIC_ERR_BOUNDS When a line is longer than AKBASIC_MAX_LINE_LENGTH or its number is out of range. + * @throws AKBASIC_ERR_BOUNDS When a line is longer than AKBASIC_MAX_LINE_LENGTH, its number is out of range, the program runs past AKBASIC_MAX_SOURCE_LINES, or an assigned number collides with a line already filed. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_load(akbasic_Runtime *obj, const char *source); @@ -734,10 +753,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime * @param obj Object to initialize, inspect, or modify. * @param lineno Line number to file it under. * @param code Source text, with any line number already stripped. + * @param numbered True when the program wrote this number, false when the loader assigned it. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKBASIC_ERR_BOUNDS When the number or the line length is out of range. */ -akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered); +/** + * @brief File a freshly-scanned line, giving it a number if it arrived without one. + * + * The one place the optional-line-number rule lives, shared by all three loading + * paths -- akbasic_runtime_load(), RUNSTREAM and `DLOAD` -- so they cannot drift. + * Reads akbasic_Runtime::hadlinenumber, which akbasic_scanner_scan() has just set, + * and akbasic_Environment::lineno, which doubles as the loader's cursor. + * + * The prompt does not use this: a line typed without a number is direct mode, not + * a line waiting for one. + * + * @param obj Object to initialize, inspect, or modify. + * @param code Source text, with any line number already stripped. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When `obj` is NULL. + * @throws AKBASIC_ERR_BOUNDS When the program runs past AKBASIC_MAX_SOURCE_LINES, or an assigned number collides with a line already filed. + */ +akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_file_line(akbasic_Runtime *obj, const char *code); /** * @brief Execute the next line of the stored program. * @param obj Object to initialize, inspect, or modify. diff --git a/src/renumber.c b/src/renumber.c index 85420e4..1bc8c82 100644 --- a/src/renumber.c +++ b/src/renumber.c @@ -308,6 +308,13 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int PASS(errctx, rewrite_line(map, obj->source[i].code, rewritten[target].code, sizeof(rewritten[target].code))); rewritten[target].lineno = target; + /* + * Every line comes out numbered, whether or not it went in that way. + * Asking for numbers is what RENUMBER is, and a program that has been + * through it can be branched into by number -- which is the whole point + * of running it over source that arrived without any. + */ + rewritten[target].numbered = true; } memcpy(obj->source, rewritten, sizeof(obj->source)); diff --git a/src/runtime.c b/src/runtime.c index 738a533..8513e37 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -955,7 +955,7 @@ int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj) return obj->environment->lineno; } -akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code) +akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered) { PREPARE_ERROR(errctx); @@ -968,6 +968,55 @@ akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lin strncpy(obj->source[lineno].code, code, AKBASIC_MAX_LINE_LENGTH - 1); obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; obj->source[lineno].lineno = lineno; + obj->source[lineno].numbered = numbered; + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext *akbasic_runtime_file_line(akbasic_Runtime *obj, const char *code) +{ + PREPARE_ERROR(errctx); + int64_t slot = 0; + + FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in file_line"); + + /* + * The scanner writes environment->lineno when it takes a number off the front + * of a line, and leaves it alone when there is none. That makes the field the + * loader's cursor as well as the current line: a numbered line moves it, and + * an unnumbered one takes the slot after wherever it now points. + * + * Before this, an unnumbered line was filed under the cursor *unchanged* -- + * that is, on top of the line before it. Two in a row silently lost the first, + * with no error and no output, which is why nothing could be written without + * numbers. + */ + if ( obj->hadlinenumber ) { + slot = obj->environment->lineno; + } else { + slot = obj->environment->lineno + 1; + FAIL_ZERO_RETURN(errctx, (slot < AKBASIC_MAX_SOURCE_LINES), AKBASIC_ERR_BOUNDS, + "A program with no line numbers may hold at most %d lines", + AKBASIC_MAX_SOURCE_LINES - 1); + } + + /* + * Refuse a collision that involves an assigned number, rather than letting one + * line quietly replace another. Only a file that mixes the two badly can reach + * this -- `500 GOTO X` after five hundred unnumbered lines -- and losing a line + * to it is not recoverable. + * + * Numbered onto numbered is left alone: a duplicate line number in a file has + * always kept the last one, and changing that is a separate decision with its + * own corpus risk. TODO.md records it. + */ + FAIL_NONZERO_RETURN(errctx, + (obj->source[slot].code[0] != '\0' + && (!obj->hadlinenumber || !obj->source[slot].numbered)), + AKBASIC_ERR_BOUNDS, + "Line %" PRId64 " was assigned to two different lines", slot); + + obj->environment->lineno = slot; + PASS(errctx, akbasic_runtime_store_line(obj, slot, code, obj->hadlinenumber)); SUCCEED_RETURN(errctx); } @@ -985,6 +1034,21 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj) SUCCEED_RETURN(errctx); } + /* + * A blank line is whitespace between statements, not a statement. Skipped + * here the way akbasic_runtime_load() and DLOAD already skip it, so all three + * loading paths agree about what a program line is. + * + * This mode used to file it like any other, under the cursor -- which for a + * blank line is the number of the line *before* it. A file ending in a blank + * line therefore had its last line erased before it ever ran, silently. The + * reference did the same, and `tests/reference/language/arithmetic/integer.bas` + * has an expectation with three values for four PRINT statements to prove it. + */ + if ( buffer[0] == '\0' ) { + SUCCEED_RETURN(errctx); + } + /* * All this mode does is pick the line number off the front and file the * source line under it. What is filed is what the scanner handed back, with @@ -999,7 +1063,7 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj) * calls LIST, which is why it went unseen. */ PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned))); - PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned)); + PASS(errctx, akbasic_runtime_file_line(obj, scanned)); SUCCEED_RETURN(errctx); } @@ -1030,7 +1094,6 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj) } obj->environment->lineno += obj->autoLineNumber; - obj->hadlinenumber = false; PASS(errctx, akbasic_scanner_scan(obj, obj->userline, scanned, sizeof(scanned))); PASS(errctx, akbasic_parser_init(&parser, obj)); obj->skiprestofline = false; @@ -1087,8 +1150,12 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj) } PASS(errctx, akbasic_runtime_interpret_immediate(obj, leaf, &value)); if ( value == NULL ) { - /* Not an immediate command, so it is program text: file it. */ - PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned)); + /* + * Not an immediate command, so it is program text: file it. Numbered by + * construction -- this arm only runs when hadlinenumber was set, because + * the branch above took every line that arrived without one. + */ + PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned, true)); } else if ( obj->autoLineNumber > 0 ) { obj->environment->lineno = akbasic_runtime_find_previous_lineno(obj); } @@ -1453,6 +1520,14 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load"); FAIL_ZERO_RETURN(errctx, (source != NULL), AKERR_NULLPOINTER, "NULL source in load"); + /* + * Start the loader's cursor at 0, so the first unnumbered line of a program + * lands on slot 1 rather than wherever a previous load left off. It also + * leaves slot 0 empty, which reads better in a LIST than a program starting + * at line 0 does. + */ + obj->environment->lineno = 0; + for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) { eol = strchr(cursor, '\n'); if ( eol == NULL ) { @@ -1478,7 +1553,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc PASS(errctx, akbasic_runtime_zero(obj)); PASS(errctx, akbasic_scanner_zero(obj)); PASS(errctx, akbasic_scanner_scan(obj, line, scanned, sizeof(scanned))); - PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned)); + PASS(errctx, akbasic_runtime_file_line(obj, scanned)); } obj->environment->nextline = 0; SUCCEED_RETURN(errctx); diff --git a/src/runtime_commands.c b/src/runtime_commands.c index 2728c7d..75766e6 100644 --- a/src/runtime_commands.c +++ b/src/runtime_commands.c @@ -577,6 +577,7 @@ akerr_ErrorContext *akbasic_cmd_delete(akbasic_Runtime *obj, akbasic_ASTLeaf *ex for ( i = startidx; i <= endidx; i++ ) { obj->source[i].code[0] = '\0'; obj->source[i].lineno = 0; + obj->source[i].numbered = false; } SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); @@ -624,6 +625,7 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { obj->source[i].code[0] = '\0'; obj->source[i].lineno = 0; + obj->source[i].numbered = false; } obj->environment->lineno = 0; obj->environment->nextline = 0; @@ -645,7 +647,7 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp * the block run with an error pending. */ PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned))); - PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned)); + PASS(errctx, akbasic_runtime_file_line(obj, scanned)); } } CLEANUP { if ( fp != NULL ) { diff --git a/src/runtime_housekeeping.c b/src/runtime_housekeeping.c index 2c0274e..f66cddc 100644 --- a/src/runtime_housekeeping.c +++ b/src/runtime_housekeeping.c @@ -88,6 +88,7 @@ akerr_ErrorContext *akbasic_cmd_new(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { obj->source[i].code[0] = '\0'; obj->source[i].lineno = 0; + obj->source[i].numbered = false; } PASS(errctx, clear_variables(obj)); PASS(errctx, akbasic_symtab_init(&obj->environment->labels, AKBASIC_MAX_LABELS)); diff --git a/src/scanner.c b/src/scanner.c index 93043a4..637e7ae 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -324,6 +324,14 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, obj->current = 0; obj->start = 0; obj->hasError = false; + /* + * Cleared here rather than by each caller, so the flag always describes the + * line this call just scanned. It used to be cleared only in + * process_line_repl(); the loading paths never touched it, which was + * harmless while they ignored it and is not now that they decide a line's + * number by it. + */ + obj->hadlinenumber = false; while ( !is_at_end(obj) && !done ) { c = obj->line[obj->current]; diff --git a/tests/reference/README.md b/tests/reference/README.md index 5543a8d..1b050c6 100644 --- a/tests/reference/README.md +++ b/tests/reference/README.md @@ -59,4 +59,5 @@ out as the thing it is. | Case | Change | Why | |---|---|---| +| `language/arithmetic/integer.txt` | Expectation grew from `4 4 2` to `4 4 2 2` | The program has **four** `PRINT` statements and the expectation had three values. The file ends in a blank line, and both interpreters filed a blank line under the loader's cursor — which for a line with no number of its own is the number of the line before it. So `40 PRINT 4 - 2` was erased before the program ran. The reference lost it the same way, which is why the expectation was written short. Fixed by skipping blank lines in RUNSTREAM, as `akbasic_runtime_load()` and `DLOAD` already did. `TODO.md` §5. | | `examples/strreverse.bas` | Variable `INPUT$` renamed to `SOURCE$` | `INPUT$` is a reserved word with a type suffix, which a real C128 refuses and which this interpreter now refuses too (`TODO.md` §6 item 16). The reference accepted it only because its own reserved-word check never fired. The expectation is byte-for-byte unchanged — the program still prints `REVERSED: OLLEH` — so the case keeps all of its value. | diff --git a/tests/reference/language/arithmetic/integer.txt b/tests/reference/language/arithmetic/integer.txt index d643e2b..d33999d 100644 --- a/tests/reference/language/arithmetic/integer.txt +++ b/tests/reference/language/arithmetic/integer.txt @@ -1,3 +1,4 @@ 4 4 2 +2 diff --git a/tests/runtime_verbs.c b/tests/runtime_verbs.c index b328ab8..3d5ec95 100644 --- a/tests/runtime_verbs.c +++ b/tests/runtime_verbs.c @@ -36,8 +36,7 @@ static void load(const char *lines[], int count) for ( i = 0; i < count; i++ ) { TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned))); - TEST_REQUIRE_OK(akbasic_runtime_store_line(&HARNESS_RUNTIME, - HARNESS_RUNTIME.environment->lineno, scanned)); + TEST_REQUIRE_OK(akbasic_runtime_file_line(&HARNESS_RUNTIME, scanned)); } } diff --git a/tests/unnumbered.c b/tests/unnumbered.c new file mode 100644 index 0000000..bee215f --- /dev/null +++ b/tests/unnumbered.c @@ -0,0 +1,251 @@ +/** + * @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 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_renumber_marks_every_line_numbered(); + return akbasic_test_failures; +}