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:
45
TODO.md
45
TODO.md
@@ -1575,6 +1575,45 @@ the reference's semantics reproduced faithfully; the third is the port's own.
|
||||
|
||||
### Found while fixing the DEF recursion hang
|
||||
|
||||
27. ~~**A call leaked one variable slot per parameter.**~~ **Fixed.**
|
||||
`akbasic_runtime_prev_environment()` released the scope but not the variables the scope
|
||||
created, so a subroutine with a local of its own exhausted the 128-slot pool after
|
||||
about 128 calls -- `Maximum runtime variables reached`, on a four-line program that
|
||||
does nothing unusual.
|
||||
|
||||
**It was there for `GOSUB` all along**, measured on a build stashed back rather than
|
||||
assumed: `FOR I# = 1 TO 300 : GOSUB 100 : NEXT` where the subroutine assigns a local
|
||||
failed before this work and passes after it. Giving each `DEF` call its own scope
|
||||
(item 26) simply made it reachable a second way, which is how it was found.
|
||||
|
||||
The release is safe because a scope's own table holds *only* what it created --
|
||||
`akbasic_environment_get()` walks up to find an outer variable and returns it without
|
||||
caching a reference. That is worth knowing before anyone adds caching there, and the
|
||||
loop says so. It is the variable *slot* that comes back, not its storage: the value
|
||||
pool never frees, so a pointer into a record `DIM`med in a scope stays sound after the
|
||||
scope is gone, which is a documented property of structures rather than an accident
|
||||
this could have taken away.
|
||||
|
||||
28. ~~**A function could not take a structure.**~~ **Done.**
|
||||
`DEF AREA(S@ AS RECT)` and `DEF POKEIT(P@ AS PTR TO RECT)` now work, and a bare
|
||||
`DEF F(S@)` is refused: `@` says "a structure" without saying which, so it does not
|
||||
state a contract the way `S$` does. Accepting it would mean checking fields at the
|
||||
call rather than at the declaration -- duck typing, and the hole naming the type
|
||||
closes. **The cost is that there are no generic functions**, one `AREA` cannot serve
|
||||
two types, and that is a real loss recorded rather than glossed.
|
||||
|
||||
Passing is by value because a parameter is bound by assignment and assignment copies;
|
||||
a pointer parameter copies its reference. Neither is a special rule. A structure
|
||||
parameter needs its storage prepared before the copy, which is the one thing a
|
||||
primitive parameter does not -- a structure variable is a run of slots and there is
|
||||
nothing to copy into until the run exists.
|
||||
|
||||
**`akbasic_value_is_truthy()` learned that a pointer is true when it points at
|
||||
something**, which had to come with it: without it there is no way to test for the end
|
||||
of a list at all, and comparing a pointer to 0 reads a numeric field it does not carry.
|
||||
A *structure* is deliberately given no truth value -- it always exists, so the question
|
||||
has no answer worth guessing at.
|
||||
|
||||
25. **A statement containing a failed multi-line `DEF` call still completes, and prints a
|
||||
junk value.** The call's error is reported and the run stops, but the *enclosing*
|
||||
statement carries on with whatever `*dest` was left holding:
|
||||
@@ -1764,12 +1803,12 @@ requirement; exactly one case has diverged on purpose since, and
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `ctest` | 106/106 — 41 reference golden cases, 20 local ones, 39 unit tests, 3 embedding examples, and `docs_examples` |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 106/106 on a machine with a display; 105 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 |
|
||||
| `ctest` | 107/107 — 41 reference golden cases, 20 local ones, 39 unit tests, 3 embedding examples, and `docs_examples` |
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 107/107 on a machine with a display; 106 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 | 106/106 |
|
||||
| ASan + UBSan | 107/107 |
|
||||
| Line coverage | 94.8% (6648/7013) — above the 90% gate |
|
||||
| Function coverage | 98.4% (441/448) |
|
||||
| Warnings | none under `-Wall -Wextra` |
|
||||
|
||||
Reference in New Issue
Block a user