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:
47
tests/language/structures/parameters.bas
Normal file
47
tests/language/structures/parameters.bas
Normal file
@@ -0,0 +1,47 @@
|
||||
10 REM A structure parameter must name its type: @ says "a structure" without
|
||||
20 REM saying which, so B@ does not state a contract the way B$ does.
|
||||
30 TYPE CRATE
|
||||
40 W#
|
||||
50 H#
|
||||
60 END TYPE
|
||||
70 DIM A@ AS CRATE
|
||||
80 A@.W# = 5
|
||||
90 A@.H# = 3
|
||||
100 DEF AREA(B@ AS CRATE) = B@.W# * B@.H#
|
||||
110 PRINT AREA(A@)
|
||||
120 REM Passing is BY VALUE, because a parameter is bound by assignment and
|
||||
130 REM assignment copies. A function cannot change its caller's record by
|
||||
140 REM accident.
|
||||
150 DEF WIDEN(B@ AS CRATE)
|
||||
160 B@.W# = 99
|
||||
170 RETURN B@.W#
|
||||
180 PRINT WIDEN(A@)
|
||||
190 PRINT A@.W#
|
||||
200 REM To change one on purpose, pass a pointer. Assignment copies a pointer's
|
||||
210 REM reference, so the callee is looking at the caller's own record.
|
||||
220 DIM Q@ AS PTR TO CRATE
|
||||
230 POINT Q@ AT A@
|
||||
240 DEF POKEIT(P@ AS PTR TO CRATE)
|
||||
250 P@->W# = 42
|
||||
260 RETURN P@->W#
|
||||
270 PRINT POKEIT(Q@)
|
||||
280 PRINT A@.W#
|
||||
290 REM And a recursive function can carry a record down with it.
|
||||
300 TYPE NODE
|
||||
310 COUNT#
|
||||
320 TAIL@ AS PTR TO NODE
|
||||
330 END TYPE
|
||||
340 DIM N1@ AS NODE
|
||||
350 DIM N2@ AS NODE
|
||||
360 N1@.COUNT# = 10
|
||||
370 N2@.COUNT# = 20
|
||||
380 POINT N1@.TAIL@ AT N2@
|
||||
390 DEF TOTAL(P@ AS PTR TO NODE)
|
||||
395 REM A pointer is true when it points at something, which is how a walk knows
|
||||
396 REM where the list ends. NOT is the bitwise operator here, so the test is
|
||||
397 REM written the positive way round.
|
||||
400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)
|
||||
410 RETURN P@->COUNT#
|
||||
420 DIM W@ AS PTR TO NODE
|
||||
430 POINT W@ AT N1@
|
||||
440 PRINT TOTAL(W@)
|
||||
6
tests/language/structures/parameters.txt
Normal file
6
tests/language/structures/parameters.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
15
|
||||
99
|
||||
5
|
||||
42
|
||||
42
|
||||
30
|
||||
@@ -128,36 +128,107 @@ static void test_single_expression_form(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A function reaches a structure through the caller's scope.
|
||||
* @brief A structure parameter is passed by value, and a pointer one is not.
|
||||
*
|
||||
* **Not through a parameter**: `DEF F(B@ AS CRATE)` is not implemented, and a
|
||||
* bare `DEF F(B@)` is refused because `@` alone does not say *which* type. So
|
||||
* the way a function works with a record today is by name, which the dynamic
|
||||
* scoping already allows -- a call's environment has the caller's as its parent.
|
||||
*
|
||||
* Pinned rather than left implicit, because it is the only route there is and a
|
||||
* reader would otherwise reasonably assume the parameter form works.
|
||||
* Neither is a special rule: a parameter is bound by assignment like any other
|
||||
* variable, and assignment copies a structure and copies a pointer's reference.
|
||||
* The two halves of the feature seen from a function's side.
|
||||
*/
|
||||
static void test_structures_reached_through_scope(void)
|
||||
static void test_structure_parameters(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 TYPE CRATE\n"
|
||||
"20 W#\n"
|
||||
"30 END TYPE\n"
|
||||
"40 DIM A@ AS CRATE\n"
|
||||
"50 A@.W# = 5\n"
|
||||
"60 DEF SCALED(N#) = A@.W# * N#\n"
|
||||
"70 PRINT SCALED(3)\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "15\n");
|
||||
"60 DEF WIDEN(B@ AS CRATE)\n"
|
||||
"70 B@.W# = 99\n"
|
||||
"80 RETURN B@.W#\n"
|
||||
"90 PRINT WIDEN(A@)\n"
|
||||
"100 PRINT A@.W#\n"));
|
||||
/* The callee saw 99; the caller still has 5, because it was a copy. */
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n5\n");
|
||||
harness_stop();
|
||||
|
||||
/* And a structure parameter is refused rather than silently misbehaving. */
|
||||
TEST_REQUIRE_OK(run_program("10 TYPE CRATE\n"
|
||||
"20 W#\n"
|
||||
"30 END TYPE\n"
|
||||
"40 DEF WID(B@) = B@.W#\n"
|
||||
"50 PRINT 1\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ERROR") != NULL,
|
||||
"a structure parameter should be refused for now, got \"%s\"", HARNESS_OUTPUT);
|
||||
"40 DIM A@ AS CRATE\n"
|
||||
"50 A@.W# = 5\n"
|
||||
"60 DIM Q@ AS PTR TO CRATE\n"
|
||||
"70 POINT Q@ AT A@\n"
|
||||
"80 DEF POKEIT(P@ AS PTR TO CRATE)\n"
|
||||
"90 P@->W# = 42\n"
|
||||
"100 RETURN P@->W#\n"
|
||||
"110 PRINT POKEIT(Q@)\n"
|
||||
"120 PRINT A@.W#\n"));
|
||||
/* And through a pointer the caller's record does change. */
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n42\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A structure parameter must name its type, and the type is checked.
|
||||
*
|
||||
* `@` says "a structure" without saying which, so `X@` does not state a contract
|
||||
* the way `X$` does. Naming the type is what closes that, and refusing a bare
|
||||
* `X@` rather than treating it as "any structure" is what keeps the rule to one
|
||||
* -- duck typing there would put the hole straight back.
|
||||
*/
|
||||
static void test_structure_parameter_types_are_checked(void)
|
||||
{
|
||||
static const struct { const char *tail; const char *expect; } CASES[] = {
|
||||
{ "90 DEF F(B@) = B@.W#\n100 PRINT 1\n", "must name its type" },
|
||||
{ "90 DEF F(B@ AS CRATE) = B@.W#\n100 PRINT F(O@)\n", "cannot take a OTHERT" },
|
||||
{ "90 DEF F(B@ AS NOSUCH) = B@.W#\n100 PRINT F(A@)\n", "which is not a type" },
|
||||
{ "90 DEF F(B@ AS PTR TO CRATE) = B@->W#\n100 PRINT F(A@)\n", "expects a pointer to" },
|
||||
{ "90 DEF F(B@ AS) = 1\n100 PRINT 1\n", "Expected a type name" }
|
||||
};
|
||||
char source[2048];
|
||||
size_t i = 0;
|
||||
|
||||
for ( i = 0; i < sizeof(CASES) / sizeof(CASES[0]); i++ ) {
|
||||
snprintf(source, sizeof(source),
|
||||
"10 TYPE CRATE\n20 W#\n30 END TYPE\n"
|
||||
"40 TYPE OTHERT\n50 N#\n60 END TYPE\n"
|
||||
"70 DIM A@ AS CRATE\n80 DIM O@ AS OTHERT\n%s", CASES[i].tail);
|
||||
TEST_REQUIRE_OK(run_program(source));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, CASES[i].expect) != NULL,
|
||||
"case %zu should say \"%s\", got \"%s\"",
|
||||
i, CASES[i].expect, HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A call gives its parameter variables back when it returns.
|
||||
*
|
||||
* One variable leaked per call before this, so a function called two hundred
|
||||
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
||||
* reached" on a four-line program. It was there for `GOSUB` all along --
|
||||
* measured on a stashed build -- and giving each `DEF` call its own scope simply
|
||||
* made it reachable a second way.
|
||||
*/
|
||||
static void test_call_scopes_are_reclaimed(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 DEF DBL(N#) = N# * 2\n"
|
||||
"20 T# = 0\n"
|
||||
"30 FOR I# = 1 TO 200\n"
|
||||
"40 T# = T# + DBL(I#)\n"
|
||||
"50 NEXT I#\n"
|
||||
"60 PRINT T#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "40200\n");
|
||||
harness_stop();
|
||||
|
||||
/* The GOSUB half, which is where the leak actually started. */
|
||||
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 300\n"
|
||||
"20 GOSUB 100\n"
|
||||
"30 NEXT I#\n"
|
||||
"40 PRINT \"OK\"\n"
|
||||
"50 END\n"
|
||||
"100 LOCALV# = I# * 2\n"
|
||||
"110 RETURN\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
@@ -170,7 +241,9 @@ int main(void)
|
||||
test_arguments_are_per_call();
|
||||
test_runaway_recursion_is_diagnosed();
|
||||
test_single_expression_form();
|
||||
test_structures_reached_through_scope();
|
||||
test_structure_parameters();
|
||||
test_structure_parameter_types_are_checked();
|
||||
test_call_scopes_are_reclaimed();
|
||||
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user