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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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