File the scanned line in RUNSTREAM, not the raw one

process_line_runstream() chose between the stripped line and the raw buffer
on obj->mode == AKBASIC_MODE_REPL, but nothing reaches that function except
the RUNSTREAM arm of step(). The stripped branch was dead, so every program
the driver ran from a file was stored with its own line number inside
source[]: LIST printed "10 10 PRINT", and DSAVE and HELP would have echoed
it the same way.

No .bas in either corpus calls LIST, and runtime_verbs.c's load() helper --
which claims to load "the way RUNSTREAM would" -- already stored the scanned
line, so it encoded the correct contract and could not catch the wrong one.
The new test drives the real sink path instead.

Keep the step-over-a-leading-number allowance in scan_line_labels() and
skip_lineno(): no path inside the library stores a raw line now, but
akbasic_runtime_store_line() is public and a host may hand it one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:25:05 -04:00
parent 812d846e47
commit d2ae0d75de
4 changed files with 66 additions and 16 deletions

View File

@@ -987,15 +987,19 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
/*
* All this mode does is pick the line number off the front and file the
* source line under it. DLOAD reaches this from REPL mode, where the line
* numbers must be stripped the same way the REPL strips them.
* source line under it. What is filed is what the scanner handed back, with
* the number stripped -- the same text `akbasic_runtime_load()` and `DLOAD`
* file, so a program is stored identically however it arrived.
*
* It used to file the raw buffer here, number and all, on the theory that
* only a REPL-driven load needed stripping. Nothing reaches this function
* except the RUNSTREAM arm of step(), so that was every file the driver
* runs: `source[]` held `10 PRINT "A"` under slot 10, and LIST, DSAVE and
* HELP would each have printed the number twice. No .bas in either corpus
* calls LIST, which is why it went unseen.
*/
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
if ( obj->mode == AKBASIC_MODE_REPL ) {
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
} else {
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, buffer));
}
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
SUCCEED_RETURN(errctx);
}
@@ -1229,12 +1233,11 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha
continue;
}
/*
* A stored line may still carry its own line number. RUNSTREAM files the
* raw text and lets the scanner strip the number again on the way to
* execution, where akbasic_runtime_load() files what the scanner already
* stripped -- so "30 LABEL X" and "LABEL X" are both real spellings of
* source[30], depending on how the program arrived. Step over the number
* without ending the statement.
* A stored line may still carry its own line number. Every path inside the
* library files what the scanner already stripped, but
* akbasic_runtime_store_line() is public and a host may hand it a raw line
* -- so "30 LABEL X" and "LABEL X" are both real spellings of source[30].
* Step over the number without ending the statement.
*/
if ( statementstart && isdigit((unsigned char)*cursor) ) {
while ( isdigit((unsigned char)*cursor) ) {

View File

@@ -42,9 +42,9 @@ static const char *skip_space(const char *cursor)
/**
* @brief Step over a stored line's own line number, if it still carries one.
*
* A line arrives either from akbasic_runtime_load(), which files what the
* scanner already stripped, or from RUNSTREAM, which files the raw text. Both
* spellings are real, so both are handled here -- the same allowance
* Every path inside the library files what the scanner already stripped, but
* akbasic_runtime_store_line() is public and a host may hand it a raw line, so
* both spellings are real and both are handled here -- the same allowance
* scan_line_labels() makes.
*/
static const char *skip_lineno(const char *cursor)