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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user