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>
This commit is contained in:
2026-08-01 11:39:47 -04:00
parent 8117fb564d
commit 6a7c8cd920
24 changed files with 1679 additions and 22 deletions

View File

@@ -171,6 +171,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_locate(struct akbasic_Runtime *ob
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_paint(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_scale(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sshape(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
/* Structures -- src/runtime_struct.c and src/parser_commands.c */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_type(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_type(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rgr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
/* Group I sound verbs -- src/runtime_audio.c */