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:
55
TODO.md
55
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <number>` 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 <label>` is unaffected.
|
||||
- **`LIST` and `DSAVE` show the numbers that were handed out**, one apart. `RENUMBER`
|
||||
before `DSAVE` if you want gaps to insert into.
|
||||
|
||||
An unnumbered program is capped at 9998 lines, which is the cap that already applied.
|
||||
|
||||
## Errors
|
||||
|
||||
`ER` and `EL` are **`ER#` and `EL#`**, ordinary global variables. `ER#` holds this
|
||||
|
||||
@@ -108,6 +108,19 @@ array slot, so `GOTO 500` is one assignment — `environment->nextline = 500`
|
||||
search. Blank slots are skipped by the step loop. That is why line numbers are capped at
|
||||
9998 and why a nine-line program still costs a 9999-entry array.
|
||||
|
||||
It is also why **a loaded line does not have to arrive with a number**. Execution needs a
|
||||
slot, not a number somebody typed, so a line that comes without one is simply given the
|
||||
slot after the last line filed. `akbasic_runtime_file_line()` is the one place that rule
|
||||
lives, shared by `akbasic_runtime_load()`, `RUNSTREAM` and `DLOAD`; the step loop, the
|
||||
branch verbs, the four prescans and `RENUMBER` never learn that anything changed, because
|
||||
all of them already worked in slots. `akbasic_SourceLine::numbered` records which kind a
|
||||
line was, and exactly one thing reads it — `akbasic_runtime_check_targets()`, which
|
||||
refuses `GOTO 100` when line 100 is a number nobody wrote.
|
||||
|
||||
The prompt is deliberately outside all of this: `process_line_repl()` files a line only
|
||||
when it *had* a number, because there the number is what separates program text from a
|
||||
statement to run now.
|
||||
|
||||
## One step
|
||||
|
||||
`akbasic_runtime_step()` is the whole loop, unrolled to a single iteration. The
|
||||
@@ -202,7 +215,7 @@ anything else sets it to `QUIT`, so `basic program.bas` exits. It is one field,
|
||||
the whole difference between an interactive session and a script runner.
|
||||
|
||||
**`akbasic_runtime_set_mode()` is not just an assignment.** Entering `REPL` prints
|
||||
`READY`. Entering `RUN` does two prescans of the whole program first:
|
||||
`READY`. Entering `RUN` does four prescans of the whole program first:
|
||||
|
||||
- **Labels.** Every `LABEL` in the source is filed before anything executes, textually
|
||||
rather than by parsing. Without it a label would exist only from the moment its `LABEL`
|
||||
@@ -211,11 +224,23 @@ the whole difference between an interactive session and a script runner.
|
||||
by label at all.
|
||||
- **`DATA` items.** `READ` walks a cursor along a list built before the program runs, so a
|
||||
`DATA` line above its `READ` is found.
|
||||
- **`TYPE` declarations.** A declaration has to be in effect wherever control goes, so
|
||||
`DIM P@ AS RECT` cannot run before `RECT` exists even if a branch skipped the lines that
|
||||
declared it.
|
||||
- **Branch targets.** The only one that reads nothing in and merely refuses: a numeric
|
||||
`GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP` or `COLLISION` target naming a line the program
|
||||
did not number. It shares `RENUMBER`'s walk in `src/renumber.c` rather than repeating it.
|
||||
|
||||
Every path into a run — `akbasic_runtime_start()`, the `RUN` verb, `CONT`, and the end of
|
||||
a `RUNSTREAM` load — goes through that one function, which is why the prescans live there
|
||||
and not in any of the four callers.
|
||||
|
||||
All four run inside one `ATTEMPT`, because **a prescan failure is the program's mistake
|
||||
and not the host's** and has to leave as a BASIC error line rather than a raised context.
|
||||
Three of them report against whichever line the loader stopped on and put the real one in
|
||||
the message text; the branch-target scan points `environment->lineno` at the line it is
|
||||
walking so the `? N :` prefix is right. `TODO.md` §5 item 64 records the difference.
|
||||
|
||||
The REPL's split between *file it* and *run it now* is one boolean: the scanner sets
|
||||
`hadlinenumber` when the line it just read began with a number. A line typed with a
|
||||
number is program text; a line typed without one is direct mode and runs immediately.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* This is the code in README.md's "Embedding the interpreter" section, kept here
|
||||
* so it is compiled by every build rather than rotting in a document. Build it
|
||||
* with -DAKBASIC_BUILD_EXAMPLES=ON and run `./build/examples/embed`.
|
||||
* with -DAKBASIC_BUILD_EXAMPLES=ON and run `./build/akbasic_example_embed`.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -114,6 +114,24 @@ static const char *PROGRAM =
|
||||
"40 NEXT I#\n"
|
||||
"50 PRINT \"DONE\"\n";
|
||||
|
||||
/*
|
||||
* The same thing with no line numbers, which is what a game script actually
|
||||
* looks like: written in a text editor, branching by LABEL, with nothing to
|
||||
* renumber. A loaded line that arrives without a number is given the slot after
|
||||
* the last one filed, so the two spellings load and run identically. Only the
|
||||
* prompt still needs numbers, because there a number is the one thing separating
|
||||
* program text from a statement to run now.
|
||||
*/
|
||||
static const char *UNNUMBERED_PROGRAM =
|
||||
"PRINT \"COUNTING:\"\n"
|
||||
"FOR I# = 1 TO 5\n"
|
||||
" PRINT I# * I#\n"
|
||||
"NEXT I#\n"
|
||||
"GOTO DONE\n"
|
||||
"PRINT \"NOT REACHED\"\n"
|
||||
"LABEL DONE\n"
|
||||
"PRINT \"DONE\"\n";
|
||||
|
||||
/*
|
||||
* One frame's worth of script. The interpreter never surrenders control: it runs
|
||||
* at most `budget` source lines and returns, so a host game keeps its frame rate
|
||||
@@ -173,6 +191,18 @@ int main(void)
|
||||
* is what the reference does too.
|
||||
*/
|
||||
CATCH(errctx, game_loop(AKBASIC_MAX_SOURCE_LINES, 8, &frames));
|
||||
printf("--- run to completion ---\n%s", CONSOLE.buffer);
|
||||
|
||||
/*
|
||||
* Then the same program again with no line numbers on it. A fresh runtime
|
||||
* rather than a second load into this one, because load() adds to whatever
|
||||
* is already filed rather than replacing it.
|
||||
*/
|
||||
CATCH(errctx, console_sink_init());
|
||||
CATCH(errctx, akbasic_runtime_init(&SCRIPT, &CONSOLE_SINK));
|
||||
CATCH(errctx, akbasic_runtime_load(&SCRIPT, UNNUMBERED_PROGRAM));
|
||||
CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
||||
CATCH(errctx, game_loop(AKBASIC_MAX_SOURCE_LINES, 8, &frames));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
@@ -180,6 +210,6 @@ int main(void)
|
||||
rc = 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
printf("--- run to completion ---\n%s", CONSOLE.buffer);
|
||||
printf("--- the same program with no line numbers ---\n%s", CONSOLE.buffer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user