diff --git a/TODO.md b/TODO.md index cab611a..1f02dae 100644 --- a/TODO.md +++ b/TODO.md @@ -1803,6 +1803,61 @@ the reference's semantics reproduced faithfully; the third is the port's own. of `DOPEN` is genuinely more useful to a program than a flat 519 — so this is not "translate everything", which is exactly why it needs deciding rather than doing. +### Found while making line numbers optional + +26. **`akbasic_environment_set_label()` files into the active scope, not the root.** + `src/environment.c:246` walks up until `obj->runtime->environment == obj`, which is the + *currently executing* scope, despite the comment above it saying "Only the top-level + environment creates labels". So a `LABEL` reached inside a `GOSUB` or `FOR` body is filed + into that scope and dies when the body pops, while `akbasic_runtime_scan_labels()` + (`src/runtime.c:1286`) files the same label into the real root. + + **The consequence is that a prescanned label and a re-filed one behave differently**, and + which one you get depends on whether control has passed through the `LABEL` statement + inside a scope. Nothing in either corpus does that, so nothing catches it. The fix is one + condition -- walk to `parent == NULL` -- plus a test that `GOSUB`s past a `LABEL` and then + branches to it from the top level. Wants checking against deviation 32 first, because + "`LABEL` still executes and re-files itself" is deliberate and the re-filing is the part + worth keeping. + +27. **Three of the four prescans report against the wrong line.** + `akbasic_runtime_set_mode()`'s handler builds its BASIC error line from + `environment->lineno`, which after a load is wherever the loader stopped -- usually the + last line of the program. The label, `DATA` and `TYPE` scans therefore print `? 47 :` for + a fault on line 3, and work around it by putting `Line %d:` in the message text + (`src/structtype.c:342`), so the message names the number twice and the prefix is wrong. + + `akbasic_runtime_check_targets()` does not have the problem because it points + `environment->lineno` at the line it is walking. That is the fix, and it belongs in the + wrapper rather than in four places: give each prescan a way to say which line it failed + on, or have each set the cursor as the target check does. Cosmetic, but it is the first + number a person reads when a program will not start. + +28. **Two lines carrying the same written line number still silently keep the last.** + `akbasic_runtime_file_line()` refuses a collision that involves an *assigned* number, but + leaves numbered-onto-numbered alone, which is what a file with `10 PRINT "A"` twice has + always done. A duplicate in a hand-written file is a mistake rather than an intent, and + refusing it would be consistent with the other three cases. + + Not changed with the rest because it is the one collision that can appear in an existing + program, so it carries corpus risk the others do not: 41 reference cases and 22 local ones + would all have to be shown clean first. `src/runtime.c`, in the `FAIL_NONZERO_RETURN` that + already names the slot. + +29. **`akbasic_renumber()` keeps two 9999-entry `static` locals.** + `src/renumber.c` holds `map[]` and `rewritten[]` at file scope, and + `akbasic_runtime_check_targets()` now adds a `static` scratch buffer beside them. That is + against the no-file-scope-mutable-state rule in `MAINTENANCE.md`, and it means `RENUMBER` + and the target prescan are not reentrant across two `akbasic_Runtime`s in one process -- + the exact thing that rule exists to guarantee. + + They are `static` because they will not fit on a default stack, which is the same reason + `akbasic_Runtime` itself is too big for one. The honest fix is to hang them off the + runtime like every other pool, which costs another ~2.5MB inline per interpreter for + something used by one verb and one prescan. Recorded rather than done, because "make it + reentrant" and "do not grow the runtime by a third for a scratch buffer" are both right + and picking between them is a decision. + ## 7. Filing gaps against `libakgl` When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in diff --git a/docs/02-getting-started.md b/docs/02-getting-started.md index db915b0..e247b06 100644 --- a/docs/02-getting-started.md +++ b/docs/02-getting-started.md @@ -12,8 +12,8 @@ READY `READY` is printed whenever the interpreter is waiting for you, which is at startup and after a program stops. -Anything you type **with a line number** is stored as part of a program. Anything you -type **without** one runs immediately: +At the prompt, anything you type **with a line number** is stored as part of a program, +and anything you type **without** one runs immediately: ```basic repl PRINT 2 + 2 @@ -23,6 +23,9 @@ PRINT 2 + 2 4 ``` +That rule is the prompt's, and only the prompt's. **A program in a file does not need +line numbers at all** — see [Line numbers](#line-numbers) below. + ## Your first program ```basic repl @@ -69,6 +72,50 @@ range, and `RENUMBER` tidies the whole program up — it rewrites every `GOTO` a `AUTO 10` turns on automatic numbering so you do not have to type them; `AUTO 0` turns it off again. +### A program in a file does not need them + +Numbers are what tells the *prompt* a program line from a statement to run now. A file +has no such problem, so a program loaded from one — or handed to the library as a string +by a host — may leave them out entirely: + +```basic +PRINT "COUNTING:" +FOR I# = 1 TO 3 + PRINT I# * I# +NEXT I# +PRINT "DONE" +``` + +```output +COUNTING: +1 +4 +9 +DONE +``` + +Lines with no number are given the next one going, so they still run in the order you +wrote them, and a blank line costs nothing. You can mix the two: a numbered line sets +where the next unnumbered one goes. + +**A program written this way branches by `LABEL`**, which is what makes it worth doing — +see [Chapter 4](04-control-flow.md#labels). Numbers you never wrote are not numbers you +can branch to, and saying so is the one thing the interpreter is strict about: + +```basic +PRINT "A" +GOTO 2 +``` + +```output +? 2 : PARSE ERROR Line 2: branch to line 2, which the program did not number. Branch by LABEL, or RENUMBER first + +``` + +`LIST` shows the numbers it handed out, so you can still edit a loaded program at the +prompt, and `DSAVE` writes them into the file. They come out one apart, with no gaps to +insert into — `RENUMBER` before you save if you want the gaps back. + ## Several statements on one line Statements are separated by colons: diff --git a/docs/04-control-flow.md b/docs/04-control-flow.md index dd127e2..21c4d14 100644 --- a/docs/04-control-flow.md +++ b/docs/04-control-flow.md @@ -183,6 +183,33 @@ ARRIVED using for its own sake: a program written with labels is immune to `RENUMBER`, because there is no number to rewrite. +Follow that one step further and the numbers stop earning their keep at all. A program +in a file can leave them out — see +[Chapter 2](02-getting-started.md#a-program-in-a-file-does-not-need-them) — and then +every branch it makes is by name: + +```basic +GOSUB GREET +GOTO FINISH + +PRINT "NOT REACHED" + +LABEL GREET +PRINT "HELLO" +RETURN + +LABEL FINISH +PRINT "DONE" +``` + +```output +HELLO +DONE +``` + +Nothing in that program can be broken by inserting a line into it, which is the whole +argument. + ## ON `ON` picks the *n*th target from a list, counting from one: diff --git a/docs/09-files-and-disk.md b/docs/09-files-and-disk.md index 051bce2..c16e9e5 100644 --- a/docs/09-files-and-disk.md +++ b/docs/09-files-and-disk.md @@ -20,6 +20,12 @@ OK `VERIFY`. A program is saved as plain text with its line numbers, so you can edit it in anything. +**`DSAVE` always writes line numbers, including ones you did not.** A program loaded +from a file that had none is given them +([Chapter 2](02-getting-started.md#a-program-in-a-file-does-not-need-them)), and those +are what gets written back. They come out one apart, so there is no room to insert +between them: `RENUMBER` before `DSAVE` if you want the gaps. + `VERIFY` compares what is in memory against the file and prints `OK`, or reports how many lines differ. diff --git a/docs/10-embedding.md b/docs/10-embedding.md index 673ee18..5bbcd18 100644 --- a/docs/10-embedding.md +++ b/docs/10-embedding.md @@ -56,6 +56,28 @@ akerr_ErrorContext AKERR_NOIGNORE *run_script(const char *source) `akbasic_runtime_run(rt, n)` runs at most `n` steps and returns. That bound is what keeps a runaway script from owning your process. +**The source you hand `akbasic_runtime_load()` does not need line numbers.** A game +script is written in a text editor and branches by `LABEL`, so numbering its lines is +work with nothing on the other end of it: + +```c wrap=hostloop +static const char *SCRIPT = + "PRINT \"SPAWNING\"\n" + "FOR I# = 1 TO HEALTH#\n" + " PRINT I#\n" + "NEXT I#\n" + "GOTO DONE\n" + "PRINT \"NOT REACHED\"\n" + "LABEL DONE\n" + "PRINT \"READY\"\n"; +``` + +Lines that carry a number are filed under it and lines that do not are given the next +one going, so the two can be mixed and a numbered script still loads exactly as it did. +What a script may *not* do is `GOTO 100` when nothing wrote a line 100 — the interpreter +refuses that before the first line runs rather than branching somewhere plausible and +wrong. + `akbasic_runtime_settime()` is how the interpreter knows what time it is. It reads no clock of its own, because it owns no loop. If you never call it, every duration expires immediately — audible, but never a hang. diff --git a/docs/13-differences.md b/docs/13-differences.md index ba26a1e..583e76b 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -92,6 +92,29 @@ A statement typed with no line number runs immediately, as it should. This was n true until recently — the interpreter used to file everything but a handful of verbs as program text. +## Line numbers + +**A program in a file does not need them.** Every BASIC this dialect descends from +required a number on every line; here that requirement belongs to the prompt alone, +where the number is the only thing separating program text from a statement to run now. +A program loaded from a file, or handed to the library as a string, may leave them out, +and a line without one is given the next number going. The two mix: a numbered line sets +where the next unnumbered one goes. + +This is QuickBASIC's idea rather than the C128's, and it is here for the same reason +QuickBASIC had it — a program that branches by `LABEL` never names a line number, so the +numbers are maintenance with nothing on the other end of it. + +Two consequences worth knowing: + +- **`GOTO ` must name a number the program wrote.** In a file with no line + numbers `GOTO 100` would otherwise find the hundredth line and branch there. It is + refused before the program runs. `GOTO