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

@@ -122,12 +122,39 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
akbasic_Environment *popped = NULL;
int i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
"No previous environment to return to");
popped = obj->environment;
obj->environment = popped->parent;
/*
* Give back the variables this scope created, as well as the scope.
*
* 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 here, so nothing in this table belongs to
* anybody else. That is worth stating, because if it ever started caching,
* this loop would free a parent's variable out from under it.
*
* It is the variable *slot* that comes back, not its storage. The value pool
* never frees, so a pointer into a record DIMmed in this scope stays sound
* after the scope is gone -- which is a documented property of structures,
* not an accident this could take away.
*
* Without it, one variable leaked per call: a function called two hundred
* times exhausted the 128-slot pool and reported "Maximum runtime variables
* reached" on a four-line program.
*/
for ( i = 0; i < popped->variables.capacity; i++ ) {
akbasic_Variable *variable = (akbasic_Variable *)popped->variables.slots[i].value;
if ( popped->variables.slots[i].used && variable != NULL ) {
variable->used = false;
}
}
/*
* Release it. The reference never does, which is a leak the GC papers over;
* here the pool is finite, so an unreleased environment is a bug that shows
@@ -758,6 +785,67 @@ akerr_ErrorContext *akbasic_runtime_interpret_immediate(akbasic_Runtime *obj, ak
SUCCEED_RETURN(errctx);
}
/**
* @brief Give a structure parameter storage in the call's scope, then fill it.
*
* A primitive parameter needs no preparation: `akbasic_environment_assign()`
* creates the variable and writes a value into it. A structure does, because a
* structure variable is a *run* of slots sized by its type and there is nothing
* to copy into until that run exists -- which is the same thing `DIM ... AS`
* does, done here on the caller's behalf.
*
* **Passing is by value, because assignment is.** A structure argument is
* deep-copied, so a function cannot change its caller's record by accident; a
* pointer argument copies the reference, which is how a function changes one on
* purpose. Neither is a special rule -- both fall out of the parameter being
* assigned like any other variable.
*/
static akerr_ErrorContext *bind_structure_parameter(akbasic_Runtime *obj, akbasic_Environment *callenv,
akbasic_ASTLeaf *param, akbasic_Value *argvalue)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
int64_t sizes[1] = { 1 };
int typeindex = -1;
bool ispointer = (param->literal_int != 0);
PASS(errctx, akbasic_structtype_find(&obj->structtypes, param->literal_string, &typeindex));
FAIL_ZERO_RETURN(errctx, (typeindex >= 0), AKBASIC_ERR_UNDEFINED,
"%s is declared AS %s, which is not a type",
param->identifier, param->literal_string);
/*
* The type check a caller actually meets. `DEF AREA(S@ AS RECT)` handed a
* COORD is refused here, by name -- which is the whole reason a structure
* parameter has to name its type rather than saying only `@`.
*/
FAIL_ZERO_RETURN(errctx,
(argvalue->valuetype == (ispointer ? AKBASIC_TYPE_POINTER : AKBASIC_TYPE_STRUCT)),
AKBASIC_ERR_TYPE,
"%s expects %s%s", param->identifier,
(ispointer ? "a pointer to " : "a "), obj->structtypes.types[typeindex].name);
FAIL_ZERO_RETURN(errctx, (argvalue->structtype == typeindex), AKBASIC_ERR_TYPE,
"%s is %s%s and cannot take a %s", param->identifier,
(ispointer ? "a pointer to " : "a "),
obj->structtypes.types[typeindex].name,
obj->structtypes.types[argvalue->structtype].name);
PASS(errctx, akbasic_environment_create(callenv, param->identifier, &variable));
sizes[0] = (ispointer ? 1 : obj->structtypes.types[typeindex].slotcount);
PASS(errctx, akbasic_variable_init(variable, &obj->valuepool, sizes, 1));
variable->valuetype = AKBASIC_TYPE_STRUCT;
variable->structtype = typeindex;
variable->ispointer = ispointer;
if ( ispointer ) {
PASS(errctx, akbasic_value_clone(argvalue, &variable->values[0]));
SUCCEED_RETURN(errctx);
}
PASS(errctx, akbasic_struct_init_slots(obj, typeindex, variable->values));
PASS(errctx, akbasic_struct_copy(obj, typeindex, argvalue->structbase, variable->values));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
@@ -808,7 +896,11 @@ akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_
while ( leafptr != NULL && argptr != NULL ) {
PASS(errctx, akbasic_runtime_evaluate(obj, leafptr, &argvalue));
obj->environment = callenv;
PASS(errctx, akbasic_environment_assign(callenv, argptr, argvalue, &unused));
if ( argptr->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT ) {
PASS(errctx, bind_structure_parameter(obj, callenv, argptr, argvalue));
} else {
PASS(errctx, akbasic_environment_assign(callenv, argptr, argvalue, &unused));
}
obj->environment = targetenv;
leafptr = leafptr->next;
argptr = argptr->next;