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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user