Document every error code a script can see, as chapter 15

ER# held numbers nothing explained. The appendix lists the four error classes
the interpreter prints, the eight codes it owns and what raises each, and the
codes that reach ER# from errno, libakerror and libakgl underneath it.

The table is not asserted. A program in the chapter trips seven of the eight and
prints what it got, and docs_examples byte-compares the result -- so the numbers
are checked rather than claimed. The eighth, 516, is not usefully trappable and
the chapter says why: entering a handler takes a scope, and the pool being empty
is what raised it.

Two things worth a reader's attention came out of writing it. Two codes register
the same ERR() text, so a program must compare the number and print the text.
And VAL reports libakerror's Value Error rather than the interpreter's 517,
which makes that number the platform's rather than ours -- filed as section 6
item 21, not fixed here, because deciding which libakstdlib failures to
translate is a boundary question and ENOENT out of DOPEN is the counter-case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:36:36 -04:00
parent 48f650c3af
commit 737fdc760f
8 changed files with 210 additions and 6 deletions

View File

@@ -25,7 +25,7 @@ scripting engine for game authors.
| [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here | | [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on | | [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it | | [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
| [`docs/`](docs/README.md) | The language itself: fourteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it | | [`docs/`](docs/README.md) | The language itself: fourteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix |
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively | | `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
| `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code | | `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code |
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** | | `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |

View File

@@ -126,7 +126,7 @@ version are catalogued in [`TODO.md`](TODO.md) and summarised for a BASIC progra
| | | | | |
|---|---| |---|---|
| [`docs/`](docs/README.md) | The guide: fourteen chapters, the language then each hardware area then a reference section for every verb and function, and [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture | | [`docs/`](docs/README.md) | The guide: fourteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, and [Chapter 15](docs/15-error-codes.md) listing every error code |
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style | | [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence | | [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
| [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it | | [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it |

32
TODO.md
View File

@@ -1502,6 +1502,27 @@ the reference's semantics reproduced faithfully; the third is the port's own.
listings read it there. Same known-failing test as item 19; the fix is a scoping decision listings read it there. Same known-failing test as item 19; the fix is a scoping decision
about where a loop counter is created, not an ordering one. about where a loop counter is created, not an ordering one.
### Found while writing the error-code appendix
21. **A dependency's status code reaches `ER#` where the interpreter has one of its own.**
`VAL("XYZ")` reports `AKERR_VALUE` — 144 on this machine — rather than
`AKBASIC_ERR_VALUE` (517), because `akbasic_fn_val()` (`src/runtime_functions.c:261`)
hands the string to `aksl_atof()` and returns whatever comes back. Both register the
name `Value Error`, so `ERR(ER#)` cannot tell them apart and only the number can.
**The consequence is that the number is not portable.** `libakerror`'s codes are offsets
from the platform's largest `errno`, so a program testing `IF ER# = 144` is right on one
machine and wrong on the next, where 517 — which is absolute — is what it should have
been able to test. `docs/15-error-codes.md` documents the behaviour as it stands and
tells a program to compare the code rather than the text.
Not fixed here because it is a decision about a boundary rather than a patch: every
place the interpreter calls into `libakstdlib` on a *program's* behalf could translate
the failure into the akbasic band, and the list of those places wants collecting before
the first one is changed. The disk verbs are the interesting counter-case — `ENOENT` out
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.
## 7. Filing gaps against `libakgl` ## 7. Filing gaps against `libakgl`
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
@@ -1824,9 +1845,16 @@ What remains, in priority order:
read the size. 320x200 is now the fallback for a backend that will not answer. Deviation 18 read the size. 320x200 is now the fallback for a backend that will not answer. Deviation 18
in §5 has the whole of it, including the off-by-one it fixed on the way. in §5 has the whole of it, including the off-by-one it fixed on the way.
11. Error codes are undefined for the user 11. ~~**Error codes are undefined for the user.**~~ **Done** — `docs/15-error-codes.md`, an
appendix covering the four error classes the interpreter prints, the eight codes it owns
with what raises each, and the codes that reach `ER#` from `errno`, `libakerror` and
`libakgl` underneath it. The table is not asserted: a program in the chapter trips seven
of the eight and prints what it got, and `docs_examples` byte-compares the result.
In the documentation, include a complete table of all possible values of ER# and EL# in the documentation in an appendix. **It found three things rather than documenting eight.** A parse error under an armed
`TRAP` vanished entirely and a trapped parse error reported `ER# 0` — both fixed, both
recorded above. And `VAL` reports a dependency's status code where the interpreter has
one of its own, which is §6 item 21 and is filed rather than fixed.
12. Screenshots for graphics operations 12. Screenshots for graphics operations

View File

@@ -231,6 +231,11 @@ Two variables are set when the trap fires: **`ER#`** is the error code and **`EL
the line it happened on. `ERR(ER#)` gives the message text. On a C128 these are called the line it happened on. `ERR(ER#)` gives the message text. On a C128 these are called
`ER` and `EL` with no suffix; this dialect has no bare variable names. `ER` and `EL` with no suffix; this dialect has no bare variable names.
**[Chapter 15](15-error-codes.md) is the complete list** of what `ER#` can hold and
what each code means. The short version is that the interpreter's own codes are 512 to
519 and are the only numbers worth comparing against; two codes render as the same
`ERR()` text, so compare the number rather than the text.
`RESUME` comes in three forms: `RESUME` comes in three forms:
| Form | Where it goes | | Form | Where it goes |

View File

@@ -54,4 +54,5 @@ this dialect has no variable name without a type suffix:
`ER#` carries this interpreter's error code, not a Commodore error number. There is no `ER#` carries this interpreter's error code, not a Commodore error number. There is no
correspondence to reproduce — the errors raised here are not the errors a 1985 ROM correspondence to reproduce — the errors raised here are not the errors a 1985 ROM
raised — so print `ERR(ER#)` rather than the number. raised — so print `ERR(ER#)` rather than the number. **[Chapter 15](15-error-codes.md)
lists every value it can hold.**

View File

@@ -85,7 +85,7 @@ program text.
`ER` and `EL` are **`ER#` and `EL#`**, ordinary global variables. `ER#` holds this `ER` and `EL` are **`ER#` and `EL#`**, ordinary global variables. `ER#` holds this
interpreter's error code, which bears no relation to a Commodore error number. Print interpreter's error code, which bears no relation to a Commodore error number. Print
`ERR(ER#)` for the text. `ERR(ER#)` for the text, and see [Chapter 15](15-error-codes.md) for the whole list.
## Graphics ## Graphics

169
docs/15-error-codes.md Normal file
View File

@@ -0,0 +1,169 @@
# 15. Appendix: error codes
Two different things get called "the error" in this interpreter, and a program author
meets both. **The report** is the line printed when something goes wrong and nothing
catches it. **The code** is the number `ER#` holds when a `TRAP` handler catches it.
This appendix is the complete list of each.
Chapter 4 is where error trapping is explained; this is the reference you come back to
once you already know what a `TRAP` is.
## The report
An error nothing traps is printed as one line and stops the run:
```basic
10 PRINT LEFT(1, 2)
20 PRINT "NEVER REACHED"
```
```output
? 10 : RUNTIME ERROR LEFT expected a string
```
The shape is `? line : CLASS message`. There are exactly four classes, and which one
you get says *when* the interpreter gave up rather than what was wrong:
| Class | Raised when |
|---|---|
| `PARSE ERROR` | The line could not be turned into a statement. Nothing on it ran. |
| `SYNTAX ERROR` | A statement was well-formed but not a legal one here. |
| `RUNTIME ERROR` | A statement ran and failed. The commonest by a wide margin. |
| `IO ERROR` | Reading the program itself failed — not a file a program opened, which is a `RUNTIME ERROR` like any other. |
The line number is where the interpreter was, which for a `PARSE ERROR` is the line
that would not parse and for a `RUNTIME ERROR` is the line that failed.
## The code
`ER#` is the code, `EL#` is the line, and `ERR(n)` is the message text for a code.
They are ordinary global variables here rather than the bare `ER` and `EL` a C128
exposes; Chapter 13 says why.
Every one of them is set for a trapped error, and **a parse error is trappable too**:
```basic
10 TRAP 200
20 DIM Q#(2)
30 SCALE
40 PRINT LEFT(1, 2)
50 GOTO NOWHERE
60 PRINT Q#(9)
70 PRINT 1 / 0
80 RESUME
90 FILTER 1, 1
100 END
200 PRINT "LINE " + EL# + ": " + ER# + " " + ERR(ER#)
210 RESUME NEXT
```
```output
LINE 30: 512 Syntax Error
LINE 40: 513 Type Error
LINE 50: 514 Undefined Reference
LINE 60: 515 Out Of Bounds
LINE 70: 517 Value Error
LINE 80: 518 State Error
LINE 90: 519 Device Error
```
That program is run by the test suite, so the numbers in the table below are checked
rather than claimed. Line 90 is `FILTER` rather than a drawing verb because `FILTER`
refuses in *every* build — a `DRAW` would only give 519 in a build with no window, and
the point here is the code rather than the verb.
## The interpreter's own codes
These eight are akbasic's, they are absolute, and they do not move. The interpreter
owns the block 512 to 767 and uses the first eight of it.
| Code | `ERR()` text | What raises it |
|---|---|---|
| 512 | `Syntax Error` | A line that will not parse, a verb given the wrong shape of arguments, a keyword where a value belongs. The commonest code and the only one that can arrive before a statement runs. |
| 513 | `Type Error` | A string where a number belongs or the reverse — `LEFT(1, 2)`, `A$ = 1 + "X"` used as a number, a non-integer array subscript. |
| 514 | `Undefined Reference` | A branch to a label or line that does not exist, or a call to a function that was never defined. |
| 515 | `Out Of Bounds` | An array subscript past the end, a colour index outside 1 to 16, a sprite number outside 1 to 8, a `RGR` or `RSPRITE` field number that is not a field. |
| 516 | `Environment Error` | The scope pool is exhausted — 32 nested `GOSUB`, `FOR` or `DEF` bodies. **See below: this is the one code you cannot usefully trap.** |
| 517 | `Value Error` | A number that is the wrong number: division by zero, a negative `SCALE` maximum, a string longer than 255 characters, a `GSHAPE` handed a string that did not come from `SSHAPE`. |
| 518 | `State Error` | A verb run outside the structure it needs — `RESUME` with no handler running, `NEXT` with no `FOR`, `LOOP` with no `DO`. |
| 519 | `Device Error` | A verb needs a graphics, audio, input or sprite device the runtime has not been given, or one that cannot do what was asked. Everything in Chapters 6, 7 and 8 refuses with this in a build with no window. `FILTER` and `SYS` refuse with it always. |
**516 is not usefully trappable and that is inherent, not a defect.** Entering a
handler takes a scope, and the pool being empty is exactly what raised the error — so
a program that exhausts it gets the report rather than the handler. Runaway recursion
is the way to reach it, and a handler that could not run is the right answer to it.
## Codes from underneath
`ER#` carries whatever code the failure was raised with, and not every failure comes
from the interpreter. Two other kinds reach a program.
**Errors from the C library, as `errno`.** The disk verbs open, read and delete real
files, and a real file refuses for the reasons files refuse:
```basic
10 TRAP 100
20 DOPEN 1, "nosuchfile.bas"
30 END
100 PRINT "" + ER# + " " + ERR(ER#)
110 END
```
```output
2 No such file or directory
```
2 is `ENOENT`. Any `errno` your platform defines can arrive this way, with the text
your C library gives it — so this is the one part of the table that is your machine's
rather than this document's.
**Errors from the libraries akbasic is built on.** `libakerror` names its own codes
just above the `errno` range, and they surface when something fails below the language
rather than in it — `SCRATCH` on a file that will not delete gives its
`Input Output Error`, and `VAL` of text that is not a number gives its `Value Error`
rather than the interpreter's 517, because the conversion happens in `libakstdlib` and
the code comes back up with it. In the SDL build, `libakgl` owns 256 to 260 and an
`SDL Error` can reach `ER#` if the renderer itself refuses.
**Do not hardcode those numbers.** `libakerror`'s codes are defined as offsets from
the largest `errno` your platform has, so the same code is a different integer on a
different machine — on the machine this was written on the range begins at 134, and
that is not a promise. The interpreter's own 512 to 519 are absolute and are the only
numbers worth writing into a program.
## Two codes render as the same text
`ERR()` gives the *registered name* for a code, and two pairs of them collide:
| Text | Given by |
|---|---|
| `Type Error` | 513, and `libakerror`'s own type error |
| `Value Error` | 517, and `libakerror`'s own value error |
The `Value Error` pair is not hypothetical — it is the `VAL` case above:
```basic
10 TRAP 100
20 PRINT VAL("XYZ")
30 END
100 PRINT ERR(ER#)
110 IF ER# = 517 THEN PRINT "INTERPRETER"
120 IF ER# <> 517 THEN PRINT "FROM UNDERNEATH"
130 END
```
```output
Value Error
FROM UNDERNEATH
```
So `IF ERR(ER#) = "Value Error" THEN` is not a reliable test and `IF ER# = 517 THEN`
is. **Compare the code; print the text.**
## What is not in `ER#`
An error the interpreter reports *about the host* rather than about your program never
reaches `ER#`, because it is not the program's error to catch — a font that will not
open, a window that will not create. Those come out of the embedding API instead;
Chapter 14 draws that line, and Chapter 10 is what a host does with it.

View File

@@ -31,6 +31,7 @@ embedding it, debugging it or changing it.
| **[12. Function reference](12-function-reference.md)** | Every function, alphabetically | | **[12. Function reference](12-function-reference.md)** | Every function, alphabetically |
| **[13. Differences from BASIC 7.0](13-differences.md)** | What a C128 programmer needs to know | | **[13. Differences from BASIC 7.0](13-differences.md)** | What a C128 programmer needs to know |
| **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it | | **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it |
| **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints |
## The shortest possible start ## The shortest possible start