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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user