Document structures: a chapter, the architecture, and the differences
docs/16-structures.md is the feature: records, nesting, copy-on-assign, strict pointers, lists, what is checked and what is not, and how a host shares its own C structs. Every example in it is executed by docs_examples and byte-compared, including the refusals -- so a message that changes fails the suite rather than quietly making the chapter wrong. The chapter makes one contrast explicitly, because it is the question a reader will actually have: a misspelled *field* is refused and a misspelled *variable* still prints zero. The rule underneath is that what the program declared gets checked and what it did not gets shrugged at -- a variable's name is never declared, a TYPE's field list is. Structures end up the strictest thing in the language, not from a higher standard but because they are the only named thing whose valid spellings are written down. Chapter 14 gains the layout: an instance is a contiguous run of value slots with a diagram of where the fields sit, the three-pass prescan and why each pass exists, why the copy cannot live in akbasic_value_clone(), and why the render depth bound is four rather than eight. Chapter 3 gains the @ suffix, chapter 13 records that all of this is an addition BASIC 7.0 has nothing like, and the verb reference gains TYPE, POINT and DIM ... AS. MAINTENANCE.md gains the two rules that are on a maintainer rather than on a test: a structure copy must not go through clone, and a field chain gets its own leaf field. TODO.md section 5 records what was invented and the three limits that are ours, and section 8 records the two defects the work exposed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
TODO.md
76
TODO.md
@@ -1021,6 +1021,52 @@ deviations from the reference's *program*: `main.go` and the SDL half of
|
||||
|
||||
### Deviations in the structure and error verbs (groups A and C)
|
||||
|
||||
61. **Structures are an addition, not a port.** BASIC 7.0 has no records at all, so
|
||||
`TYPE`/`END TYPE`, `DIM X@ AS T`, `.` and `->`, `PTR TO` and `POINT ... AT` are
|
||||
invented here. The `@` suffix was not: the Go reference reserved
|
||||
`IDENTIFIER_STRUCT` and never used it, and `src/grammar.c` rendered such a leaf as
|
||||
`"NOT IMPLEMENTED"` until this landed. `docs/16-structures.md` is the whole feature.
|
||||
|
||||
**Assignment copies and `POINT` shares**, which is the decision everything else rests
|
||||
on. A structure is a value like every other value here, so a program that never writes
|
||||
`POINT` can never be surprised by aliasing — and the two field operators are kept
|
||||
apart so a reader always knows from the spelling which they are looking at. `.` on a
|
||||
pointer and `->` on a value are both errors, each naming the other.
|
||||
|
||||
**A declared type is what buys the storage model.** An instance has a known slot count
|
||||
and is laid out exactly as an array is, out of the same value pool, so nothing new
|
||||
holds data — only a table of descriptors. It also bounds copy depth statically, since a
|
||||
`TYPE` cannot contain itself by value.
|
||||
|
||||
**Three limits are ours and are stated rather than derived:** 16 types, 16 fields per
|
||||
type, and four levels of nesting in `PRINT`. The last is not a safety bound on copying —
|
||||
copy stops at pointers by construction — but on *rendering*, which must follow a
|
||||
pointer and would not come back on a cycle. Four rather than eight because the bound
|
||||
has to bite before the 256-byte render buffer does, or a cycle stops because it ran out
|
||||
of room rather than because it was told to.
|
||||
|
||||
**A type name shares a namespace with verbs and labels**, since all three are bare
|
||||
words. Refused at declaration with a message that says so, because the parser's own
|
||||
answer was "Expected expression or literal" pointing at the line rather than the
|
||||
problem. Field names follow the same reserved-word rule variable names already do,
|
||||
enforced by the loader's scan — `TO@` is a bad field name for exactly the reason `TO#`
|
||||
is a bad variable name.
|
||||
|
||||
62. **A host's C struct is the same thing with its bytes somewhere else.**
|
||||
`akbasic_host_register_type()` puts it in the *same* table a `TYPE` fills, so copy,
|
||||
`PTR TO`, `.` and `->` all work across the boundary with no second set of rules — and
|
||||
the language's own copy-versus-`POINT` distinction turns out to be exactly the
|
||||
snapshot-versus-share distinction a host needs, so there is one API rather than two.
|
||||
|
||||
A binding takes a **shadow run** of slots; a read refreshes from host memory and a
|
||||
write converts back. Conversion **refuses rather than truncates** — 70000 into an
|
||||
`int16_t` names the field — and a field name's suffix must agree with the C type it
|
||||
describes, refused at registration.
|
||||
|
||||
**It is the only pointer this interpreter holds that it did not allocate**, and that
|
||||
is the one thing a host has to think about. `akbasic_host_unbind()` exists for it.
|
||||
`examples/hoststruct.c` is a working host, built and run by every build.
|
||||
|
||||
44. **A whole loop on one line does not loop.** `DO : PRINT 1 : LOOP` runs once, exactly as
|
||||
`FOR I=1 TO 3 : PRINT I : NEXT I` does. Block skipping walks *source lines*, which is the
|
||||
reference's `waitingForCommand` model (§1.6), so a `LOOP` on the same line as its `DO` is
|
||||
@@ -1527,6 +1573,24 @@ 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
|
||||
about where a loop counter is created, not an ordering one.
|
||||
|
||||
### Found while building structures
|
||||
|
||||
23. ~~**A host could not run one script twice.**~~ **Fixed.**
|
||||
`akbasic_runtime_start()` never rewound: `RUN` sets `nextline = 0` and clears
|
||||
`stopped`, and `start()` set neither. That cost nothing while a host started a script
|
||||
once, because `nextline` is already 0 the first time — and cost everything to a host
|
||||
running one script per enemy, where the second `start()` began past the end and
|
||||
silently did nothing at all. `start` means start now. Found by writing
|
||||
`examples/hoststruct.c`, which is exactly that shape.
|
||||
|
||||
24. ~~**The prescan wiped host-registered types.**~~ **Fixed.** It runs again on every
|
||||
`RUN` and used to reinitialise the whole table, so the first `RUN` unregistered
|
||||
everything the host had registered before loading — with no way for the host to know
|
||||
it had to register them again. It now drops what the *script* declared and keeps what
|
||||
the *host* registered, which works because host types are always a prefix and so every
|
||||
index a field already recorded keeps pointing at the same type.
|
||||
`tests/hoststruct.c` pins it.
|
||||
|
||||
### Found while auditing `akbasic_value_clone()` for the structures work
|
||||
|
||||
22. ~~**The numeric operators' `else` was a catch-all, not a float branch.**~~ **Fixed.**
|
||||
@@ -1648,14 +1712,14 @@ requirement; exactly one case has diverged on purpose since, and
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `ctest` | 95/95 — 41 reference golden cases, 15 local ones, 36 unit tests, 2 embedding examples, and `docs_examples` |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 95/95 on a machine with a display; 94 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
|
||||
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 41 programs, 9 transcripts, 49 output comparisons, 2 excerpts, 2 shell blocks and 8 figures in the default build; 57 programs and 48 output comparisons in the AKGL one. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
|
||||
| `ctest` | 104/104 — 41 reference golden cases, 20 local ones, 39 unit tests, 3 embedding examples, and `docs_examples` |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 104/104 on a machine with a display; 103 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
|
||||
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 49 programs, 9 transcripts, 57 output comparisons, 2 excerpts, 2 shell blocks and 8 figures in the default build; 65 programs and 56 output comparisons in the AKGL one. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
|
||||
| `docs_screenshots` | 8/8 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
|
||||
| Golden corpus | 41/41 byte-exact from `tests/reference/` — **and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output |
|
||||
| ASan + UBSan | 95/95 |
|
||||
| Line coverage | 95.1% (5916/6222) — above the 90% gate |
|
||||
| Function coverage | 98.5% (403/409) |
|
||||
| ASan + UBSan | 104/104 |
|
||||
| Line coverage | 94.8% (6648/7013) — above the 90% gate |
|
||||
| Function coverage | 98.4% (441/448) |
|
||||
| Warnings | none under `-Wall -Wextra` |
|
||||
| `doxygen Doxyfile` | clean |
|
||||
| Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` |
|
||||
|
||||
Reference in New Issue
Block a user