Add records: TYPE, DIM ... AS, field access, copy on assign

BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was
not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used
it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now.

Declaring the type is what buys the storage model. A TYPE states its fields, so
an instance has a known slot count and is laid out exactly as an array is -- one
contiguous run from the same value pool DIM already draws from, with field access
as offset arithmetic. No new pool holds data; the only new table holds
descriptors. A nested value flattens into its container's run, which is why
LINE with two POINTs and a string is five slots rather than three.

Each field takes its type from its own suffix, the same rule every other name
here follows, so a field list needs no type column. An `@` field is the
exception and has to name its type, because three primitive types fit in three
suffix characters and N declared types do not fit in one.

The declaration is prescanned before the program runs, like labels and DATA and
for the same reason: it has to be in effect wherever control goes. Three passes,
each for a case the one before cannot do -- names first so a field can refer to
a type declared later, then field lists, then sizes by repeated resolution. What
never resolves is a cycle of by-value containment, so "a TYPE cannot contain
itself by value" is a diagnosis rather than an assumption, and the message says
to use PTR TO instead.

Assignment copies. That interception is the whole feature and it cannot live in
akbasic_value_clone(), which copies one slot -- and one slot holds a *reference*
to an instance rather than the instance, so going through it would alias. A
structure is intercepted before that path and its slots are copied one at a
time, walking the descriptor rather than memcpy-ing the run, because a pointer
field must copy its reference where a value field must copy its slots.

Two smaller things the work required. All three prescans now sit inside one
ATTEMPT: a malformed declaration is the program's mistake, and it was printing a
stack trace and taking the driver with it, which is the boundary goal 3 exists
to draw. And a fresh variable's structtype is -1 rather than the 0 a memset
leaves, because 0 is a valid type index and every new variable was claiming to
be the first type declared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-08-01 11:39:47 -04:00
parent 7c1bee431a
commit 4f86a9ca44
24 changed files with 1679 additions and 22 deletions

View File

@@ -357,6 +357,66 @@ static akerr_ErrorContext *collect_subscripts(akbasic_Environment *obj, akbasic_
SUCCEED_RETURN(errctx);
}
/**
* @brief Assign into a field, checking the field's declared type as it goes.
*
* The type check is the same one a variable gets, and it comes from the same
* place: the field's suffix said what it holds when the TYPE was declared. What
* is different is that a *field* name is checked for existence at all, which a
* variable name never is -- the set of fields is closed and written down, and
* akbasic_structtype_field() lists them when one is missed.
*/
static akerr_ErrorContext *assign_field(akbasic_Environment *obj, akbasic_ASTLeaf *lval,
akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_StructField *field = NULL;
akbasic_Value *slot = NULL;
PASS(errctx, akbasic_struct_resolve(obj->runtime, lval, &field, &slot));
switch ( field->kind ) {
case AKBASIC_FIELD_STRUCT:
FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_STRUCT), AKBASIC_ERR_TYPE,
"Incompatible types in assignment to %s", field->name);
FAIL_ZERO_RETURN(errctx, (rval->structtype == field->typeindex), AKBASIC_ERR_TYPE,
"%s is a %s and cannot be assigned a %s", field->name,
obj->runtime->structtypes.types[field->typeindex].name,
obj->runtime->structtypes.types[rval->structtype].name);
PASS(errctx, akbasic_struct_copy(obj->runtime, field->typeindex, rval->structbase, slot));
break;
case AKBASIC_FIELD_POINTER:
FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_POINTER), AKBASIC_ERR_TYPE,
"%s is a pointer; POINT it AT a structure rather than assigning one to it",
field->name);
FAIL_ZERO_RETURN(errctx, (rval->structtype == field->typeindex), AKBASIC_ERR_TYPE,
"%s points to %s and cannot hold a pointer to %s", field->name,
obj->runtime->structtypes.types[field->typeindex].name,
obj->runtime->structtypes.types[rval->structtype].name);
PASS(errctx, akbasic_value_clone(rval, slot));
break;
default:
if ( field->valuetype == AKBASIC_TYPE_INTEGER && rval->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_zero(slot));
slot->valuetype = AKBASIC_TYPE_INTEGER;
slot->intval = (int64_t)rval->floatval;
break;
}
if ( field->valuetype == AKBASIC_TYPE_FLOAT && rval->valuetype == AKBASIC_TYPE_INTEGER ) {
PASS(errctx, akbasic_value_zero(slot));
slot->valuetype = AKBASIC_TYPE_FLOAT;
slot->floatval = (double)rval->intval;
break;
}
FAIL_ZERO_RETURN(errctx, (rval->valuetype == field->valuetype), AKBASIC_ERR_TYPE,
"Incompatible types in assignment to %s", field->name);
PASS(errctx, akbasic_value_clone(rval, slot));
break;
}
*dest = slot;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_environment_assign(akbasic_Environment *obj, akbasic_ASTLeaf *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
@@ -368,6 +428,16 @@ akerr_ErrorContext *akbasic_environment_assign(akbasic_Environment *obj, akbasic
FAIL_ZERO_RETURN(errctx, (obj != NULL && lval != NULL && rval != NULL && dest != NULL),
AKERR_NULLPOINTER, "nil pointer");
/*
* A field is addressed by walking the chain, not by looking a name up in a
* scope -- `E@.POS@.X#` names no variable called `X#`. So it is resolved and
* assigned here, before the by-name lookup the rest of this function does.
*/
if ( lval->leaftype == AKBASIC_LEAF_FIELD ) {
PASS(errctx, assign_field(obj, lval, rval, dest));
SUCCEED_RETURN(errctx);
}
PASS(errctx, akbasic_environment_get(obj, lval->identifier, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
"Identifier %s is undefined", lval->identifier);
@@ -382,6 +452,46 @@ akerr_ErrorContext *akbasic_environment_assign(akbasic_Environment *obj, akbasic
PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &slot));
switch ( lval->leaftype ) {
case AKBASIC_LEAF_IDENTIFIER_STRUCT:
/*
* **This is the whole of copy-on-assign**, and it has to happen here
* rather than in akbasic_value_clone(): a clone copies one slot, and one
* slot holds a *reference* to an instance rather than the instance. Going
* through clone would therefore alias -- exactly the reference semantics
* the language does not have -- so a structure is intercepted before it
* reaches that path and its slots are copied one at a time.
*
* A pointer variable is the opposite and takes the clone: copying a
* pointer copies the reference, which is what makes `POINT` the only way
* to share and assignment always a copy.
*/
FAIL_ZERO_RETURN(errctx, (variable->structtype >= 0), AKBASIC_ERR_STATE,
"%s has not been DIMmed AS a type", lval->identifier);
if ( variable->ispointer ) {
FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_POINTER),
AKBASIC_ERR_TYPE,
"%s is a pointer; POINT it AT a structure rather than assigning one to it",
lval->identifier);
FAIL_ZERO_RETURN(errctx, (rval->structtype == variable->structtype),
AKBASIC_ERR_TYPE,
"%s points to %s and cannot hold a pointer to %s",
lval->identifier,
obj->runtime->structtypes.types[variable->structtype].name,
obj->runtime->structtypes.types[rval->structtype].name);
PASS(errctx, akbasic_value_clone(rval, slot));
break;
}
FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_STRUCT), AKBASIC_ERR_TYPE,
"Incompatible types in variable assignment");
FAIL_ZERO_RETURN(errctx, (rval->structtype == variable->structtype), AKBASIC_ERR_TYPE,
"%s is a %s and cannot be assigned a %s",
lval->identifier,
obj->runtime->structtypes.types[variable->structtype].name,
obj->runtime->structtypes.types[rval->structtype].name);
PASS(errctx, akbasic_struct_copy(obj->runtime, variable->structtype,
rval->structbase, slot));
*dest = slot;
SUCCEED_RETURN(errctx);
case AKBASIC_LEAF_IDENTIFIER_INT:
if ( rval->valuetype == AKBASIC_TYPE_INTEGER ) {
PASS(errctx, akbasic_variable_set_integer(variable, rval->intval, subscripts, subscriptcount));