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