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

@@ -673,6 +673,90 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **
* so the environment is told to skip forward to that RETURN rather than execute
* the body during the definition.
*/
/**
* @brief A `DEF` parameter list, which is not an ordinary argument list.
*
* `akbasic_parser_argument_list()` parses *expressions*, and a parameter is a
* declaration -- `S@ AS RECT` is not an expression and stops that parser dead
* with "Unbalanced parenthesis". So this reads the list itself.
*
* **A structure parameter must name its type.** `@` says "a structure" without
* saying which: three primitive types fit in three suffix characters and N
* declared types do not fit in one, so `X$` fully states its contract and `X@`
* does not. A bare `X@` is refused rather than treated as "any structure",
* because that is duck typing and it puts back exactly the hole naming the type
* closes. The cost is that there are no generic functions, and it is a real one.
*
* The type name is parked on the parameter leaf in `literal_string`, with
* `literal_int` recording `PTR TO` -- the same two free fields, for the same
* reason, that `DIM ... AS` uses.
*/
static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *arglist = NULL;
akbasic_ASTLeaf *param = NULL;
akbasic_ASTLeaf *tail = NULL;
akbasic_Token *word = NULL;
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN),
AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF <name>");
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
do {
PASS(errctx, akbasic_parser_primary(parser, &param));
FAIL_ZERO_RETURN(errctx,
(param != NULL &&
(param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
param->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
param->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT ||
param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT)),
AKBASIC_ERR_SYNTAX,
"Only variable identifiers are valid arguments for DEF");
if ( param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT ) {
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX,
"%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME",
param->identifier, param->identifier, param->identifier);
PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX,
"Expected AS after %s, not %s", param->identifier, word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
PASS(errctx, akbasic_parser_previous(parser, &word));
if ( strcasecmp(word->lexeme, "PTR") == 0 ) {
param->literal_int = 1;
/* `TO` is already a verb -- FOR ... TO owns it -- so it arrives
as a command token rather than an identifier. */
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX,
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
PASS(errctx, akbasic_parser_previous(parser, &word));
}
snprintf(param->literal_string, sizeof(param->literal_string), "%s", word->lexeme);
}
if ( tail == NULL ) {
arglist->right = param;
} else {
tail->next = param;
}
tail = param;
} while ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) );
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_RIGHT_PAREN),
AKBASIC_ERR_SYNTAX, "Expected ) after the argument list");
*dest = arglist;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
@@ -680,7 +764,6 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
akbasic_ASTLeaf *identifier = NULL;
akbasic_ASTLeaf *arglist = NULL;
akbasic_ASTLeaf *expression = NULL;
akbasic_ASTLeaf *walk = NULL;
akbasic_ASTLeaf *command = NULL;
akbasic_FunctionDef *fndef = NULL;
size_t i = 0;
@@ -689,18 +772,7 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected identifier");
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, true, &arglist));
FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX,
"Expected argument list (identifier names)");
for ( walk = arglist->right; walk != NULL; walk = walk->next ) {
FAIL_ZERO_RETURN(errctx,
(walk->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
walk->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
walk->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
AKBASIC_ERR_SYNTAX,
"Only variable identifiers are valid arguments for DEF");
}
PASS(errctx, parse_def_parameters(parser, &arglist));
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));