Let DEF take structure parameters, and give call scopes back

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>
This commit is contained in:
2026-08-01 13:05:32 -04:00
parent 00daa17a47
commit c1d06ee4b8
8 changed files with 452 additions and 40 deletions

View File

@@ -186,15 +186,86 @@ Note line 130: assigning one *pointer* to another copies the reference, not the
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 recursive `DEF` works too, and is bounded by the scope pool at 32 deep — the same
bound `GOSUB` has. What a function cannot yet do is take a structure *parameter*:
`DEF F(B@ AS NODE)` is not implemented, and a bare `DEF F(B@)` is refused because `@`
alone does not say which type. Until it is, a function reaches a record by name, which
the scoping already allows — a call can see the caller's variables.
**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