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>
This commit is contained in:
2026-08-01 16:33:27 -04:00
parent d2ae0d75de
commit 8bc253ebbf
12 changed files with 428 additions and 14 deletions

View File

@@ -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.