From 6bac929901b431eb35c34f489978719ec67aa5cf Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 1 Aug 2026 12:03:02 -0400 Subject: [PATCH] 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) --- MAINTENANCE.md | 21 +++ TODO.md | 76 +++++++++- docs/03-the-language.md | 6 + docs/11-verb-reference.md | 3 + docs/13-differences.md | 11 ++ docs/14-architecture.md | 77 ++++++++++ docs/16-structures.md | 295 ++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 8 files changed, 484 insertions(+), 6 deletions(-) create mode 100644 docs/16-structures.md diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 222c7f8..ab959d5 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -497,6 +497,27 @@ So: still do it during single-threaded init, before the host game spawns anythin because the call would race, but because there is one operation in the registry that cannot be made safe and this is the discipline that avoids needing it. +### Structures reuse the array machinery, deliberately + +`TYPE` declares a record, so an instance has a known slot count and takes one contiguous +run from the same value pool `DIM A#(10)` draws from. **There is no structure pool**, and +adding one would be the wrong instinct: the only new table holds *descriptors* — names and +slot offsets — and the data goes where array data already goes. + +Two rules for changing any of it: + +- **A structure copy must not go through `akbasic_value_clone()`.** Clone copies one slot, + and one slot holds a *reference* to an instance rather than the instance, so a structure + taking that path aliases instead of copying — which is the semantics the language + deliberately does not have. `akbasic_environment_assign()` intercepts first and calls + `akbasic_struct_copy()`. If you add a place a structure can be assigned, it goes through + that, not through clone. +- **A field chain gets its own leaf type and its own link field.** `include/akbasic/grammar.h` + records three separate defects that came from giving one link field two meanings; + `AKBASIC_LEAF_FIELD` keeps its base on `.left`, which nothing else on that leaf type uses. + +`docs/14-architecture.md` has the layout diagram and the three-pass prescan. + ### Nothing calls malloc `libakgl`'s hard rule, and ours: obtain objects from `akgl_heap_next_*` and release them back, diff --git a/TODO.md b/TODO.md index 71fc847..70ee4de 100644 --- a/TODO.md +++ b/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` | diff --git a/docs/03-the-language.md b/docs/03-the-language.md index 2d24a62..5cd0eff 100644 --- a/docs/03-the-language.md +++ b/docs/03-the-language.md @@ -10,6 +10,7 @@ a character that says what it holds: | `#` | integer | `COUNT#`, `I#` | | `%` | floating point | `RATE%`, `X%` | | `$` | string | `NAME$` | +| `@` | structure | `ENEMY@` | ```basic 10 COUNT# = 42 @@ -20,6 +21,11 @@ a character that says what it holds: There is no such thing as a variable with no suffix. A bare name is a **label** — see Chapter 4 — so `GOTO DONE` and `LABEL DONE` are how the two meet. +`@` is the odd one out: it says "a structure" without saying *which*, so a structure +variable has to be declared with `DIM E@ AS ENEMY` before it can be used. That is +**[Chapter 16](16-structures.md)**, along with records, pointers and how a host shares its +own C structs with a script. + On a C128 the suffixes mean something different (`%` is integer, no suffix is float). Here `%` is float and `#` is integer, following the Go implementation this was ported from. Chapter 13 lists it with the other differences. diff --git a/docs/11-verb-reference.md b/docs/11-verb-reference.md index 1808845..66324b5 100644 --- a/docs/11-verb-reference.md +++ b/docs/11-verb-reference.md @@ -33,6 +33,7 @@ for the reasoning in each case. | `DEF` | `DEF NAME(args) = expr` | Define a function. Multi-line definitions end in `RETURN`. | | `DELETE` | `DELETE [n][-n]` | Delete lines, with the same range forms as `LIST`. | | `DIM` | `DIM A#(n [,...])` | Make an array. Subscripts start at zero; `n` is the count. | +| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. | | `DIRECTORY` | `DIRECTORY` | **Refused.** Needs a directory-reading wrapper that does not exist yet. | | `DLOAD` | `DLOAD "name"` | Load a program from a file. | | `DO` | `DO [WHILE c | UNTIL c]` | Start a loop. The condition may be here, on the `LOOP`, or neither. | @@ -70,6 +71,7 @@ for the reasoning in each case. | `ON` | `ON e GOTO|GOSUB t [,...]` | Branch to the `e`th target, counting from one. | | `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. | | `PLAY` | `PLAY "notes"` | Queue notes. Does not block. | +| `POINT` | `POINT P@ AT s@` | Aim a strict pointer at a structure. See Chapter 16. | | `POKE` | `POKE addr, byte` | Write a byte to a real address. | | `PRINT` | `PRINT [expr]` | Print a value and a newline. | | `PRINT#` | `PRINT #n, expr` | Write a line to a channel. | @@ -101,6 +103,7 @@ for the reasoning in each case. | `TRAP` | `TRAP [target]` | Send errors to a handler. No target disarms it. | | `TROFF` | `TROFF` | Turn line tracing off. | | `TRON` | `TRON` | Turn line tracing on; each line prints its number in brackets. | +| `TYPE` | `TYPE NAME` … `END TYPE` | Declare a record, its fields one per line. See Chapter 16. | | `VERIFY` | `VERIFY "name"` | Compare the program in memory against a file. | | `VOL` | `VOL n` | Set the overall volume, 0 to 15. | | `WAIT` | `WAIT addr, mask [,xor]` | Poll a byte until it matches. Holds the program. | diff --git a/docs/13-differences.md b/docs/13-differences.md index 0e4cdd3..ba26a1e 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -42,6 +42,17 @@ There is no escape character. Integers are 64-bit and floats are IEEE doubles, so `PRINT 1.5` gives `1.500000`. A leading zero is not octal; `0x` is hexadecimal. +### Structures are an addition + +BASIC 7.0 has no records at all. `TYPE`/`END TYPE`, `DIM X@ AS T`, the `@` suffix, `.` and +`->`, `PTR TO` and `POINT ... AT` are all new here, and **[Chapter 16](16-structures.md)** +is the whole of it. Nothing about it changes an existing program. + +Two consequences a C128 programmer should know. A structure assignment **copies**, like +every other assignment; sharing is spelled `POINT`. And a type name is a bare word, so it +shares a namespace with verbs and labels — `TYPE POINT` is refused because `POINT` is now a +verb. + ## Block structure **A whole loop on one line does not loop.** diff --git a/docs/14-architecture.md b/docs/14-architecture.md index 99cbc10..2cf4599 100644 --- a/docs/14-architecture.md +++ b/docs/14-architecture.md @@ -421,6 +421,83 @@ normally true, and evaluating `A#` then yields a clone drawn from the per-line v own evaluation. If you are debugging an assignment that wrote to the wrong place, that flag is the first thing to check. +## Structures are laid out exactly as arrays are + +The whole storage model falls out of one decision: **a `TYPE` is declared**, so an +instance has a known slot count before the program runs. + +That means a structure needs no pool of its own. `DIM R@ AS RECT` calls the same +`akbasic_valuepool_take()` that `DIM A#(10)` calls, and the variable's `values` run *is* +the instance. A field access is offset arithmetic against an offset the type descriptor +already knows. A nested value field **flattens into its container's run** — a `SEGMENT` +holding two `COORD`s and a string is five slots, not three — which is why the nesting is +free rather than a second indirection. + +```text + DIM S@ AS SHAPE SHAPE: NAME$, ORIGIN@ AS COORD, AREA% + COORD: X#, Y# + + variable S@ + structtype ──► type table entry SHAPE (slotcount 4) + values ──────► ┌────────┬────────┬────────┬────────┐ + │ NAME$ │ X# │ Y# │ AREA% │ + └────────┴────────┴────────┴────────┘ + off 0 off 1 off 2 off 3 + └── ORIGIN@ is offset 1, two slots ──┘ +``` + +`src/structtype.c` fills the table, and it does so **in three passes**, each for a case +the pass before cannot handle. Names first, so a field can refer to a type declared +further down. Then field lists, now able to resolve every reference. Then sizes, by +repeated resolution — a type whose fields are all sized can be sized, and repeating that +settles any legal ordering. **Whatever never resolves is a cycle of by-value containment**, +which is how "a `TYPE` cannot contain itself by value" is a diagnosis rather than an +assumption. + +It is a *prescan*, run from `akbasic_runtime_set_mode()` beside the label and `DATA` +scans, for the reason all three are: a declaration has to be in effect wherever control +goes, including when a branch skips the lines that made it. The `TYPE` verb's whole job at +run time is to **jump past its own `END TYPE`**, because the field lines are declarations +and executing `W#` would evaluate a bare identifier and quietly create a global. + +### Copy, and where it has to happen + +A `STRUCT` value and a `POINTER` value carry the same thing: a type index and a base. What +differs is what *assignment* does with it. + +**The copy cannot live in `akbasic_value_clone()`**, and this is the trap to know about. +Clone copies one slot, and one slot holds a *reference* to an instance rather than the +instance — so a structure going through clone would alias, which is precisely the +semantics the language does not have. `akbasic_environment_assign()` intercepts a +structure before that path and calls `akbasic_struct_copy()`, which walks the descriptor +and copies slot by slot. Deliberately not one `memcpy` of the run: a pointer field must +copy its reference where a value field must copy its slots, and only the descriptor knows +which is which. + +Copy is therefore **deep through values and stops at pointers**, as it is for a C struct +holding a pointer. Since a `TYPE` cannot contain itself by value, copy depth is fixed by +the type graph before the program starts and no copy can recurse away. Only *rendering* +needs a runtime bound, because a pointer can make the graph cyclic — that is +`AKBASIC_MAX_STRUCT_DEPTH`, four, chosen so the bound bites before the 256-byte render +buffer does. + +### A host structure is the same thing with its bytes somewhere else + +`akbasic_host_register_type()` puts a host's C struct in the *same* table, so `PTR TO`, +copy-on-assign, `.` and `->` all work across the boundary with no second set of rules. The +only difference is where a field's bytes live, and that is carried by three members on the +field descriptor — `hostkind`, `hostoffset`, `hostwidth`. + +A binding takes a **shadow run** of slots from the ordinary pool. A field read refreshes +its slot from host memory first; a field write converts back and stores. So the script +always sees current values and its writes always land, while everything else in the +interpreter goes on seeing one storage model. If you are debugging a host binding that +reads stale data, `akbasic_host_refresh()` is where to look. + +**This is the one pointer the interpreter holds that it did not allocate.** Everything +else is pool-bounded; a binding whose instance has been freed is the only way to get a +wild pointer in here, which is what `akbasic_host_unbind()` is for. + ## Errors come in two kinds, and the distinction is the point ```text diff --git a/docs/16-structures.md b/docs/16-structures.md new file mode 100644 index 0000000..ae7c63d --- /dev/null +++ b/docs/16-structures.md @@ -0,0 +1,295 @@ +# 16. Structures + +A structure groups values that belong together. Commodore BASIC 7.0 has nothing like +it — this is entirely an addition, and Chapter 13 lists it with the other differences. + +A structure variable's name ends in **`@`**, the fourth type suffix: + +| Suffix | Type | +|---|---| +| `#` | integer | +| `%` | floating point | +| `$` | string | +| `@` | structure | + +## Declaring a type + +`TYPE` … `END TYPE` names a record and lists its fields, one per line. **Each field takes +its type from its own suffix**, the same rule every other name in this language follows, +so a field list needs no type column: + +```basic +10 TYPE RECT +20 W# +30 H# +40 END TYPE +50 DIM R@ AS RECT +60 R@.W# = 3 +70 R@.H# = 4 +80 PRINT R@.W# * R@.H# +90 PRINT R@ +``` + +```output +12 +RECT(W#=3, H#=4) +``` + +`DIM name@ AS TYPE` is how a variable gets storage, and it is required — unlike an +ordinary variable, a structure cannot spring into existence on first use, because +nothing would say which type it is. + +**A type name is a bare word, and so is every verb**, so the two share a namespace. +`TYPE POINT` is refused, because `POINT` is a verb: + +```basic +10 TYPE POINT +20 X# +30 END TYPE +40 PRINT 1 +``` + +```output +? 40 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type + +``` + +The same applies to field names, for the same reason a variable cannot be called `TO#`. + +## Nesting + +A field may be another structure, named with `AS`: + +```basic +10 TYPE COORD +20 X# +30 Y# +40 END TYPE +50 TYPE SHAPE +60 NAME$ +70 ORIGIN@ AS COORD +80 END TYPE +90 DIM S@ AS SHAPE +100 S@.NAME$ = "BOX" +110 S@.ORIGIN@.X# = 10 +120 PRINT S@ +``` + +```output +SHAPE(NAME$=BOX, ORIGIN@=COORD(X#=10, Y#=0)) +``` + +`@` on its own says "a structure" but not *which*, which is why a structure field has to +name its type where `W#` does not. Three primitive types fit in three suffix characters; +ten declared types do not fit in one. + +## Assignment copies + +**This is the rule to remember.** A structure behaves like every other value here: + +```basic +10 TYPE RECT +20 W# +30 END TYPE +40 DIM A@ AS RECT +50 DIM B@ AS RECT +60 A@.W# = 1 +70 B@ = A@ +80 A@.W# = 99 +90 PRINT B@.W# +``` + +```output +1 +``` + +`B@` is its own record from line 70 onward. Nesting copies too — a whole record, however +deep, moves as a unit. + +## Pointers + +When you want two names for *one* record, say so. A pointer is a distinct declared kind: + +```basic +10 TYPE RECT +20 W# +30 END TYPE +40 DIM A@ AS RECT +50 DIM P@ AS PTR TO RECT +60 A@.W# = 1 +70 POINT P@ AT A@ +80 P@->W# = 99 +90 PRINT A@.W# +``` + +```output +99 +``` + +Three things are deliberate: + +- **`POINT` is the only way to share.** A program that never writes it can never be + surprised by aliasing. +- **`.` reaches a field of a structure and `->` reaches one through a pointer.** They do + not stand in for one another, and using the wrong one is an error that names the other. + So a reader always knows from the spelling whether the thing on the left is their own + copy or somebody else's data. +- **A pointer with nothing behind it is `NOTHING`**, and dereferencing it is refused + rather than being a crash. + +```basic +10 TYPE RECT +20 W# +30 END TYPE +40 DIM P@ AS PTR TO RECT +50 PRINT P@ +60 PRINT P@->W# +``` + +```output +NOTHING +? 60 : RUNTIME ERROR This pointer is not pointing at anything yet; POINT it AT a structure first + +``` + +## Lists and trees + +A `TYPE` may refer to **itself only through a pointer** — by value it would have no +finite size, and that is refused at declaration. Which is exactly what makes a list +possible: + +```basic +10 TYPE NODE +20 COUNT# +30 TAIL@ AS PTR TO NODE +40 END TYPE +50 DIM N1@ AS NODE +60 DIM N2@ AS NODE +70 N1@.COUNT# = 10 +80 N2@.COUNT# = 20 +90 POINT N1@.TAIL@ AT N2@ +100 DIM WALK@ AS PTR TO NODE +110 POINT WALK@ AT N1@ +120 PRINT WALK@->COUNT# +130 WALK@ = WALK@->TAIL@ +140 PRINT WALK@->COUNT# +150 PRINT N1@ +``` + +```output +10 +20 +NODE(COUNT#=10, TAIL@=NODE(COUNT#=20, TAIL@=NOTHING)) +``` + +Note line 130: assigning one *pointer* to another copies the reference, not the record. +That is the one place assignment does not deep-copy, and it is why pointers are declared +separately rather than being a mode a structure can be in. + +**Walk a list with a loop, not with a recursive `DEF`.** A recursive multi-line `DEF` +does not return in this interpreter; it is a known defect recorded in `TODO.md`, and it +is not specific to structures. + +`PRINT` follows pointers, so a cycle would not come back — it stops after four levels and +prints `(...)`. + +## What is checked, and what is not + +A field name is checked against the set the type declared, and the refusal lists the +fields that do exist: + +```basic +10 TYPE RECT +20 W# +30 H# +40 END TYPE +50 DIM R@ AS RECT +60 PRINT TOTLA# +70 PRINT R@.NOPE# +``` + +```output +0 +? 70 : RUNTIME ERROR RECT has no field NOPE# (W#, H#) + +``` + +Line 60 is the contrast worth understanding. **A misspelled variable is still silent** — +`TOTLA#` prints zero, as it does in every BASIC ever written. A misspelled *field* is +not, because the set of fields is closed and the program wrote it down. + +That is the rule underneath both: **what the program declared gets checked, and what it +did not gets shrugged at.** A variable's name is never declared, so it cannot be checked. +A `TYPE`'s field list is, so it can be. Structures end up the strictest thing in the +language, not because they are held to a higher standard but because they are the only +named thing whose valid spellings are written down. + +## Sharing a structure with a host + +If you are embedding the interpreter in a game, a script can read and write **the game's +own C structs** — not a copy of them. The host describes its struct once: + +```c norun +typedef struct +{ + char name[32]; + int32_t hp; + float x; + bool hostile; +} game_Enemy; + +static const akbasic_HostField ENEMY_FIELDS[] = { + /* struct member BASIC name C representation */ + AKBASIC_HOST_FIELD( game_Enemy, name, "NAME$", AKBASIC_HOSTFIELD_CSTRING ), + AKBASIC_HOST_FIELD( game_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ), + AKBASIC_HOST_FIELD( game_Enemy, x, "X%", AKBASIC_HOSTFIELD_FLOAT ), + AKBASIC_HOST_FIELD( game_Enemy, hostile, "HOSTILE#", AKBASIC_HOSTFIELD_BOOL ) +}; + +static const akbasic_HostType ENEMY_TYPE = { + "ENEMY", sizeof(game_Enemy), ENEMY_FIELDS, 4 +}; + +akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE); +akbasic_host_bind(&SCRIPT, "FOE@", "ENEMY", &GOBLIN); +``` + +and the script then works on `FOE@` like any other structure: + +```basic norun +10 PRINT FOE@.NAME$ + " HAS " + FOE@.HP# +20 FOE@.HP# = FOE@.HP# - 10 +``` + +Line 20 decrements `GOBLIN.hp` in place. There is no marshalling step. + +`examples/hoststruct.c` is a complete working host, built and run by every build. +Chapter 10 covers the rest of the embedding API; the parts specific to structures: + +| Call | Does | +|---|---| +| `akbasic_host_register_type` | Makes a C struct available as a BASIC type | +| `akbasic_host_bind` | Binds one instance to a script variable | +| `akbasic_host_rebind` | Points that name at a different instance — the per-frame call | +| `akbasic_host_unbind` | Breaks the binding before the storage goes away | + +Three things a host should know: + +- **Conversion refuses rather than truncates.** Assigning 70000 to an `int16_t` field, or + forty characters to a `char[32]`, is an error naming the field. +- **A host `float` round-trips lossily**, because BASIC floats are doubles. A `char[]` has + a width that BASIC strings do not. +- **A bound instance must outlive the binding.** It is the only pointer this interpreter + holds that it did not allocate; `akbasic_host_unbind()` exists for exactly that. + +## Limits + +| | | +|---|---| +| Types | 16 | +| Fields per type | 16 | +| Nesting shown by `PRINT` | 4 levels | + +An instance's fields come out of the same value pool arrays use, so the 4096-element +budget in Chapter 13 covers both. **Nothing is reclaimed** — a structure lasts until +`CLR` or `NEW`, exactly as an array does. diff --git a/docs/README.md b/docs/README.md index 7a5c03a..daec805 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,6 +32,7 @@ embedding it, debugging it or changing it. | **[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 | | **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints | +| **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host | ## The shortest possible start