Document optional line numbers

Chapter 2 gets the rule and the refusal, chapter 4 gets the payoff for
LABEL, chapter 9 says DSAVE writes the numbers it handed out and to
RENUMBER first if you want gaps, chapter 10 shows a host loading numberless
source, chapter 13 gets the QuickBASIC-shaped divergence, and chapter 14's
source[] passage gets its second half.

Chapter 14 said "two prescans" and listed two; there were three before this
and there are four now, so it lists all four and says which of them reports
against the right line.

TODO.md section 6 records four things found on the way and deliberately not
fixed: set_label() filing into the active scope rather than the root, three
prescans reporting the wrong line number, duplicate written line numbers
still replacing silently, and renumber.c's file-scope scratch arrays.

examples/embed.c runs the same program twice, numbered and not, so the
example compiles the feature rather than describing it. Its header pointed
at ./build/examples/embed, which is not where the binary lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:43:14 -04:00
parent 6273be580a
commit 28ea99a638
8 changed files with 240 additions and 5 deletions

55
TODO.md
View File

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