DEF AREA(S@ AS RECT) = S@.W# * S@.H#
DEF POKEIT(P@ AS PTR TO RECT)
A parameter names its type, exactly as DIM does. A bare `DEF F(S@)` is refused:
@ says "a structure" without saying which, so it does not state a contract the
way S$ does, and accepting it would mean checking fields at the call rather than
at the declaration -- which is the hole naming the type closes. The cost is that
there are no generic functions, and that is a real loss rather than an oversight.
Passing is by value, because a parameter is bound by assignment and assignment
copies; a pointer parameter copies its reference and lets a function change its
caller's record on purpose. Neither is a special rule. What a structure
parameter does need is its storage prepared before the copy, since a structure
variable is a run of slots and there is nothing to copy into until the run
exists.
A DEF parameter list is no longer parsed as an argument list, because a
parameter is a declaration rather than an expression: `S@ AS RECT` stopped that
parser dead with "Unbalanced parenthesis".
akbasic_value_is_truthy() learned that a pointer is true when it points at
something, which had to come with this. Without it there is no way to test for
the end of a list at all -- comparing a pointer to 0 reads a numeric field it
does not carry and answers whatever that field held. A structure is deliberately
given no truth value: it always exists, so the question has no answer worth
guessing at.
And a regression I introduced last commit, plus the older one underneath it.
prev_environment() released a scope but not the variables the scope created, so
a call leaked one slot per parameter and two hundred calls exhausted the
128-slot pool. Giving each DEF call its own scope made that reachable; it was
there for GOSUB all along, measured on a stashed build -- a subroutine with a
local of its own failed after about 128 calls before any of this work. The
release is safe because a scope's table holds only what it created, and it is
the variable slot that comes back rather than its storage, so a pointer into a
record DIMmed in that scope stays sound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
369 lines
9.4 KiB
Markdown
369 lines
9.4 KiB
Markdown
# 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.
|
|
|
|
**A pointer is true when it points at something**, which is how a walk knows where the
|
|
list ends — and a recursive `DEF` can do the walking:
|
|
|
|
```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 DEF TOTAL(P@ AS PTR TO NODE)
|
|
110 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)
|
|
120 RETURN P@->COUNT#
|
|
130 DIM W@ AS PTR TO NODE
|
|
140 POINT W@ AT N1@
|
|
150 PRINT TOTAL(W@)
|
|
```
|
|
|
|
```output
|
|
30
|
|
```
|
|
|
|
`NOT` is the bitwise operator here, so write the test the positive way round as line 110
|
|
does. Recursion is bounded by the scope pool at 32 deep, the same bound `GOSUB` has.
|
|
|
|
`PRINT` follows pointers, so a cycle would not come back — it stops after four levels and
|
|
prints `(...)`.
|
|
|
|
## Passing a structure to a function
|
|
|
|
A parameter names its type, exactly as `DIM` does:
|
|
|
|
```basic
|
|
10 TYPE CRATE
|
|
20 W#
|
|
30 H#
|
|
40 END TYPE
|
|
50 DIM A@ AS CRATE
|
|
60 A@.W# = 5
|
|
70 A@.H# = 3
|
|
80 DEF AREA(B@ AS CRATE) = B@.W# * B@.H#
|
|
90 PRINT AREA(A@)
|
|
```
|
|
|
|
```output
|
|
15
|
|
```
|
|
|
|
**A bare `B@` is refused.** `@` says "a structure" without saying which, so it does not
|
|
state a contract the way `B$` does — and accepting it would mean checking fields at the
|
|
call instead of at the declaration, which is the hole naming the type closes. The cost is
|
|
that there are no generic functions: one `AREA` cannot serve `CRATE` and `BOXY`.
|
|
|
|
**Passing is by value**, because a parameter is bound by assignment and assignment copies:
|
|
|
|
```basic
|
|
10 TYPE CRATE
|
|
20 W#
|
|
30 END TYPE
|
|
40 DIM A@ AS CRATE
|
|
50 A@.W# = 5
|
|
60 DEF WIDEN(B@ AS CRATE)
|
|
70 B@.W# = 99
|
|
80 RETURN B@.W#
|
|
90 PRINT WIDEN(A@)
|
|
100 PRINT A@.W#
|
|
```
|
|
|
|
```output
|
|
99
|
|
5
|
|
```
|
|
|
|
The function saw 99; the caller still has 5. To change a caller's record on purpose, pass
|
|
a pointer — `DEF POKEIT(P@ AS PTR TO CRATE)` — and reach through it with `->`. Neither is
|
|
a special rule: both fall out of the parameter being assigned like any other variable.
|
|
|
|
## 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.
|