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:
@@ -300,6 +300,7 @@ set(AKBASIC_TESTS
|
|||||||
struct_types
|
struct_types
|
||||||
trap_verbs
|
trap_verbs
|
||||||
symtab
|
symtab
|
||||||
|
unnumbered
|
||||||
user_functions
|
user_functions
|
||||||
value_arithmetic
|
value_arithmetic
|
||||||
value_bitwise
|
value_bitwise
|
||||||
|
|||||||
30
TODO.md
30
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
|
it had not been used for, and both were filed rather than worked around silently. One came
|
||||||
back fixed.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -45,10 +45,20 @@ typedef enum
|
|||||||
AKBASIC_ERRCLASS_RUNTIME
|
AKBASIC_ERRCLASS_RUNTIME
|
||||||
} akbasic_ErrorClass;
|
} 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
|
typedef struct
|
||||||
{
|
{
|
||||||
char code[AKBASIC_MAX_LINE_LENGTH];
|
char code[AKBASIC_MAX_LINE_LENGTH];
|
||||||
int64_t lineno;
|
int64_t lineno;
|
||||||
|
bool numbered;
|
||||||
} akbasic_SourceLine;
|
} akbasic_SourceLine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -665,15 +675,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_start(akbasic_Runtime *obj, i
|
|||||||
* its source text.
|
* its source text.
|
||||||
*
|
*
|
||||||
* Lines are separated by `\n`; a `\r` before it is tolerated. Blank lines are
|
* 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
|
* skipped.
|
||||||
* what the scanner does in RUNSTREAM mode. This does not run anything: follow it
|
*
|
||||||
* with akbasic_runtime_start(obj, AKBASIC_MODE_RUN).
|
* **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 obj Object to initialize, inspect, or modify.
|
||||||
* @param source Whole program text; must not be NULL.
|
* @param source Whole program text; must not be NULL.
|
||||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
* @throws AKERR_NULLPOINTER When `obj` or `source` is NULL.
|
* @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);
|
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 obj Object to initialize, inspect, or modify.
|
||||||
* @param lineno Line number to file it under.
|
* @param lineno Line number to file it under.
|
||||||
* @param code Source text, with any line number already stripped.
|
* @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.
|
* @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.
|
* @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.
|
* @brief Execute the next line of the stored program.
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
|||||||
@@ -308,6 +308,13 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int
|
|||||||
PASS(errctx, rewrite_line(map, obj->source[i].code,
|
PASS(errctx, rewrite_line(map, obj->source[i].code,
|
||||||
rewritten[target].code, sizeof(rewritten[target].code)));
|
rewritten[target].code, sizeof(rewritten[target].code)));
|
||||||
rewritten[target].lineno = target;
|
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));
|
memcpy(obj->source, rewritten, sizeof(obj->source));
|
||||||
|
|||||||
@@ -955,7 +955,7 @@ int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj)
|
|||||||
return obj->environment->lineno;
|
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);
|
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);
|
strncpy(obj->source[lineno].code, code, AKBASIC_MAX_LINE_LENGTH - 1);
|
||||||
obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0';
|
obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0';
|
||||||
obj->source[lineno].lineno = lineno;
|
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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,6 +1034,21 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
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
|
* 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
|
* 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.
|
* calls LIST, which is why it went unseen.
|
||||||
*/
|
*/
|
||||||
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
|
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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,7 +1094,6 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj)
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj->environment->lineno += obj->autoLineNumber;
|
obj->environment->lineno += obj->autoLineNumber;
|
||||||
obj->hadlinenumber = false;
|
|
||||||
PASS(errctx, akbasic_scanner_scan(obj, obj->userline, scanned, sizeof(scanned)));
|
PASS(errctx, akbasic_scanner_scan(obj, obj->userline, scanned, sizeof(scanned)));
|
||||||
PASS(errctx, akbasic_parser_init(&parser, obj));
|
PASS(errctx, akbasic_parser_init(&parser, obj));
|
||||||
obj->skiprestofline = false;
|
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));
|
PASS(errctx, akbasic_runtime_interpret_immediate(obj, leaf, &value));
|
||||||
if ( value == NULL ) {
|
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 ) {
|
} else if ( obj->autoLineNumber > 0 ) {
|
||||||
obj->environment->lineno = akbasic_runtime_find_previous_lineno(obj);
|
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, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load");
|
||||||
FAIL_ZERO_RETURN(errctx, (source != NULL), AKERR_NULLPOINTER, "NULL source 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) ) {
|
for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) {
|
||||||
eol = strchr(cursor, '\n');
|
eol = strchr(cursor, '\n');
|
||||||
if ( eol == NULL ) {
|
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_runtime_zero(obj));
|
||||||
PASS(errctx, akbasic_scanner_zero(obj));
|
PASS(errctx, akbasic_scanner_zero(obj));
|
||||||
PASS(errctx, akbasic_scanner_scan(obj, line, scanned, sizeof(scanned)));
|
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;
|
obj->environment->nextline = 0;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
|
|||||||
@@ -577,6 +577,7 @@ akerr_ErrorContext *akbasic_cmd_delete(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
|
|||||||
for ( i = startidx; i <= endidx; i++ ) {
|
for ( i = startidx; i <= endidx; i++ ) {
|
||||||
obj->source[i].code[0] = '\0';
|
obj->source[i].code[0] = '\0';
|
||||||
obj->source[i].lineno = 0;
|
obj->source[i].lineno = 0;
|
||||||
|
obj->source[i].numbered = false;
|
||||||
}
|
}
|
||||||
SUCCEED_TRUE(obj, dest);
|
SUCCEED_TRUE(obj, dest);
|
||||||
SUCCEED_RETURN(errctx);
|
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++ ) {
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
||||||
obj->source[i].code[0] = '\0';
|
obj->source[i].code[0] = '\0';
|
||||||
obj->source[i].lineno = 0;
|
obj->source[i].lineno = 0;
|
||||||
|
obj->source[i].numbered = false;
|
||||||
}
|
}
|
||||||
obj->environment->lineno = 0;
|
obj->environment->lineno = 0;
|
||||||
obj->environment->nextline = 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.
|
* the block run with an error pending.
|
||||||
*/
|
*/
|
||||||
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
|
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 {
|
} CLEANUP {
|
||||||
if ( fp != NULL ) {
|
if ( fp != NULL ) {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ akerr_ErrorContext *akbasic_cmd_new(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
||||||
obj->source[i].code[0] = '\0';
|
obj->source[i].code[0] = '\0';
|
||||||
obj->source[i].lineno = 0;
|
obj->source[i].lineno = 0;
|
||||||
|
obj->source[i].numbered = false;
|
||||||
}
|
}
|
||||||
PASS(errctx, clear_variables(obj));
|
PASS(errctx, clear_variables(obj));
|
||||||
PASS(errctx, akbasic_symtab_init(&obj->environment->labels, AKBASIC_MAX_LABELS));
|
PASS(errctx, akbasic_symtab_init(&obj->environment->labels, AKBASIC_MAX_LABELS));
|
||||||
|
|||||||
@@ -324,6 +324,14 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
|
|||||||
obj->current = 0;
|
obj->current = 0;
|
||||||
obj->start = 0;
|
obj->start = 0;
|
||||||
obj->hasError = false;
|
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 ) {
|
while ( !is_at_end(obj) && !done ) {
|
||||||
c = obj->line[obj->current];
|
c = obj->line[obj->current];
|
||||||
|
|||||||
@@ -59,4 +59,5 @@ out as the thing it is.
|
|||||||
|
|
||||||
| Case | Change | Why |
|
| 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. |
|
| `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. |
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
4
|
4
|
||||||
4
|
4
|
||||||
2
|
2
|
||||||
|
2
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ static void load(const char *lines[], int count)
|
|||||||
for ( i = 0; i < count; i++ ) {
|
for ( i = 0; i < count; i++ ) {
|
||||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
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_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned)));
|
||||||
TEST_REQUIRE_OK(akbasic_runtime_store_line(&HARNESS_RUNTIME,
|
TEST_REQUIRE_OK(akbasic_runtime_file_line(&HARNESS_RUNTIME, scanned));
|
||||||
HARNESS_RUNTIME.environment->lineno, scanned));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
251
tests/unnumbered.c
Normal file
251
tests/unnumbered.c
Normal file
@@ -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 <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 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user