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

@@ -60,6 +60,8 @@ typedef enum
AKBASIC_TOK_FUNCTION_ARGUMENT, /* 38 */
AKBASIC_TOK_ATSYMBOL, /* 39 */
AKBASIC_TOK_IDENTIFIER_STRUCT, /* 40 */
AKBASIC_TOK_DOT, /* 41 -- field of a structure value */
AKBASIC_TOK_ARROW, /* 42 -- field through a pointer */
/*
* 41, 42 -- MOVSPR's two separators, and nothing else in the language uses
* either. Appended rather than filed in with the other punctuation because
@@ -91,7 +93,8 @@ typedef enum
AKBASIC_LEAF_FUNCTION, /* 17 */
AKBASIC_LEAF_BRANCH, /* 18 */
AKBASIC_LEAF_ARGUMENTLIST, /* 19 */
AKBASIC_LEAF_IDENTIFIER_STRUCT /* 20 */
AKBASIC_LEAF_IDENTIFIER_STRUCT, /* 20 */
AKBASIC_LEAF_FIELD /* 21 -- `base . name`; base on .left */
} akbasic_LeafType;
typedef struct
@@ -338,6 +341,20 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_string(akbasic_ASTLe
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_identifier(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype, const char *lexeme);
/**
* @brief Build a field access: `base . name`, or `base -> name`.
*
* @param obj Leaf to initialize.
* @param base The expression the field is read from; stored on `.left`.
* @param name The field name, suffix included.
* @param operator_ AKBASIC_TOK_DOT or AKBASIC_TOK_ARROW, which is what says
* whether the base must be a structure or a pointer to one.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `base` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the name is too long.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_field(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *base, const char *name, akbasic_TokenType operator_);
/**
* @brief Render a leaf the way the reference's toString() does.
*

View File

@@ -29,6 +29,7 @@
#include <akbasic/input.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
#include <akbasic/structtype.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
@@ -106,6 +107,8 @@ typedef struct akbasic_Runtime
akbasic_Variable variables[AKBASIC_MAX_VARIABLES];
akbasic_FunctionDef functions[AKBASIC_MAX_FUNCTIONS];
akbasic_ValuePool valuepool;
/* Every TYPE the program declared, prescanned like labels and DATA. */
akbasic_StructTypeTable structtypes;
akbasic_Value staticTrueValue;
akbasic_Value staticFalseValue;

View File

@@ -0,0 +1,180 @@
/**
* @file structtype.h
* @brief Declares the table of `TYPE` declarations and how a field is found in one.
*
* BASIC 7.0 has no records at all, so none of this is a port -- see TODO.md
* section 5 for what was invented and why.
*
* **Declaring the type is what buys the storage model.** Because a `TYPE` states
* its fields up front, an instance has a known slot count and is laid out
* exactly as an array is: one contiguous run taken from the runtime's value pool
* by akbasic_valuepool_take(). A field access is then offset arithmetic against
* an offset this table already knows, and no new pool is needed for the data --
* only this table of descriptors, which holds names and offsets rather than
* values.
*
* A field nested **by value** flattens into its parent's run at an offset. A
* field declared `PTR TO` occupies one slot holding a reference. That is the
* whole difference, and it is why a `TYPE` may refer to itself only through a
* pointer: by value it would have no finite size, which is refused at
* declaration rather than discovered when the pool runs out.
*/
#ifndef _AKBASIC_STRUCTTYPE_H_
#define _AKBASIC_STRUCTTYPE_H_
#include <akerror.h>
#include <akbasic/grammar.h>
#include <akbasic/types.h>
struct akbasic_Runtime;
/** @brief What a field holds, which is what says how much room it needs. */
typedef enum
{
AKBASIC_FIELD_PRIMITIVE = 0, /* one slot, type from the name's suffix */
AKBASIC_FIELD_STRUCT, /* nested by value; flattened into the parent */
AKBASIC_FIELD_POINTER /* one slot holding a reference */
} akbasic_FieldKind;
/** @brief One declared field: what it is called, what it holds, and where it sits. */
typedef struct
{
char name[AKBASIC_MAX_STRUCT_NAME]; /** as written, suffix included */
akbasic_FieldKind kind;
akbasic_Type valuetype; /** for a primitive, from the suffix */
int typeindex; /** for STRUCT and POINTER */
int offset; /** slots from the start of the instance */
int slotcount; /** slots this field occupies */
} akbasic_StructField;
/**
* @brief One `TYPE` declaration.
*
* `firstline` and `lastline` are the `TYPE` and `END TYPE` source lines. They
* are kept because the block has to be *skipped* at run time: its field lines
* are declarations, and letting them execute would evaluate each field name as a
* bare identifier and quietly create a global of that name.
*/
typedef struct
{
char name[AKBASIC_MAX_STRUCT_NAME];
akbasic_StructField fields[AKBASIC_MAX_STRUCT_FIELDS];
int fieldcount;
int slotcount; /** total slots for one instance */
int64_t firstline;
int64_t lastline;
bool used;
} akbasic_StructType;
/** @brief Every `TYPE` a program declared. Lives on the runtime. */
typedef struct
{
akbasic_StructType types[AKBASIC_MAX_STRUCT_TYPES];
int count;
} akbasic_StructTypeTable;
/**
* @brief Empty the table.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_structtype_table_init(akbasic_StructTypeTable *obj);
/**
* @brief Find a declared type by name.
* @param obj Table to search.
* @param name Type name, without a suffix.
* @param dest Output destination populated by the function; the index, or -1.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_structtype_find(akbasic_StructTypeTable *obj, const char *name, int *dest);
/**
* @brief Resolve a field of a declared type, refusing one it does not declare.
*
* The refusal lists the fields the type *does* have, because the whole reason a
* misspelled field can be caught at all is that the set is closed and written
* down -- so the diagnostic may as well show it.
*
* @param obj Table the type lives in.
* @param typeindex Which type.
* @param name Field name, suffix included.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj`, `name` or `dest` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `typeindex` names no type.
* @throws AKBASIC_ERR_UNDEFINED When the type does not declare that field.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_structtype_field(akbasic_StructTypeTable *obj, int typeindex, const char *name, akbasic_StructField **dest);
/**
* @brief Read every `TYPE` block out of the program before it runs.
*
* Called from akbasic_runtime_set_mode() beside the label and `DATA` prescans,
* and for the same reason all three are prescans: a declaration has to be in
* effect wherever control happens to go, including when a branch skips over the
* lines that made it. It also means `DIM P@ AS RECT` can never run before the
* type it names exists.
*
* @param obj The runtime, whose source is read and whose table is filled.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_SYNTAX When a block is malformed or unterminated.
* @throws AKBASIC_ERR_VALUE When a type contains itself by value, or redeclares a name.
* @throws AKBASIC_ERR_BOUNDS When the table or a field list is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_structtype_scan(struct akbasic_Runtime *obj);
/**
* @brief Which `TYPE` block a source line opens, or -1.
*
* How `TYPE` skips its own body at run time: the verb asks what block it is
* looking at and jumps past its `END TYPE`.
*
* @param obj Table to search.
* @param lineno The line the interpreter is on.
* @param dest Output destination populated by the function; the index, or -1.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `dest` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_structtype_block_at(akbasic_StructTypeTable *obj, int64_t lineno, int *dest);
/* --------------------------------------------------- the runtime half --- */
/**
* @brief Fill @p dest with a description of an instance: which type, and where.
*
* What a structure-valued *expression* evaluates to. Not the data -- an instance
* is a run of slots and a value is one slot -- so this is the reference that
* lets assignment find the slots to copy, or lets `.` step into them.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_struct_describe(struct akbasic_Runtime *obj, int typeindex, akbasic_Value *base, bool pointer, akbasic_Value *dest);
/**
* @brief Copy one instance's slots onto another's.
*
* Deep through nested values, and stops at pointers -- as it does for a C struct
* holding a pointer. A `TYPE` cannot contain itself by value, so the depth is
* fixed by the type graph before the program runs and this cannot recurse away.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_struct_copy(struct akbasic_Runtime *obj, int typeindex, akbasic_Value *src, akbasic_Value *dest);
/**
* @brief Resolve a field-access leaf to the slot it addresses.
*
* Enforces the strict-pointer rule on the way: `.` requires a structure on its
* left and `->` requires a pointer, and neither stands in for the other.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_struct_resolve(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_StructField **fielddest, akbasic_Value **slotdest);
/** @brief Evaluate a field access to the value it yields. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_struct_evaluate_field(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);
/** @brief Render an instance the way PRINT does, bounded by depth. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_struct_to_string(struct akbasic_Runtime *obj, int typeindex, akbasic_Value *base, int depth, char *dest, size_t len);
#endif // _AKBASIC_STRUCTTYPE_H_

View File

@@ -39,6 +39,23 @@
*/
#define AKBASIC_MAX_CONDITION_LEAVES 16
/*
* Structure types. These bound the *descriptors* -- names and slot offsets --
* and not the data: an instance's fields are ordinary values drawn from the same
* AKBASIC_MAX_ARRAY_VALUES pool an array uses, because a declared TYPE has a
* known slot count and so is laid out exactly as an array is.
*/
#define AKBASIC_MAX_STRUCT_TYPES 16 /* distinct TYPE declarations */
#define AKBASIC_MAX_STRUCT_FIELDS 16 /* fields in one TYPE */
#define AKBASIC_MAX_STRUCT_NAME 32 /* a type or field name */
/*
* How deep a chain of by-value nesting may go. A TYPE cannot contain itself by
* value -- that is refused at declaration -- so this is not what stops infinite
* recursion; it bounds the *rendering* of a structure, where a pointer can make
* the graph cyclic and PRINT would otherwise not come back.
*/
#define AKBASIC_MAX_STRUCT_DEPTH 8
/* Commodore convention: true is -1, not 1. */
#define AKBASIC_TRUE -1
#define AKBASIC_FALSE 0
@@ -56,7 +73,9 @@ typedef enum
AKBASIC_TYPE_INTEGER, /* 1 -- identifier suffix '#' */
AKBASIC_TYPE_FLOAT, /* 2 -- identifier suffix '%' */
AKBASIC_TYPE_STRING, /* 3 -- identifier suffix '$' */
AKBASIC_TYPE_BOOLEAN /* 4 */
AKBASIC_TYPE_BOOLEAN, /* 4 */
AKBASIC_TYPE_STRUCT, /* 5 -- identifier suffix '@' */
AKBASIC_TYPE_POINTER /* 6 -- identifier suffix '@', declared PTR TO */
} akbasic_Type;
/*
@@ -76,6 +95,23 @@ typedef struct akbasic_Value
double floatval;
int64_t boolvalue;
bool mutable_;
/*
* What a STRUCT or POINTER value carries. Not a payload: a *reference* to
* one, because an instance is a run of slots and cannot fit in the one slot
* a value occupies.
*
* These are their own members rather than a reuse of `intval`, and that is
* deliberate -- section 6 item 5 was an operator reading a numeric field
* that happened to be the wrong one, and putting a slot index somewhere
* arithmetic already looks would set the same trap again.
*
* The distinction between the two types is what an *assignment* does with
* this reference, not what the reference is: a STRUCT copies the slots it
* points at, and a POINTER copies the reference itself.
*/
int structtype; /* index into the type table, -1 for none */
struct akbasic_Value *structbase; /* first slot of the instance */
} akbasic_Value;
#endif // _AKBASIC_TYPES_H_

View File

@@ -26,6 +26,14 @@ typedef struct
int dimensioncount;
bool mutable_;
bool used; /** Pool bookkeeping */
/*
* What DIM ... AS recorded. A structure variable's `values` run is one
* instance laid out by the type's offsets, exactly as an array's run is its
* elements -- so these two are what tell the difference between the two
* kinds of run.
*/
int structtype; /** type index, -1 when not a structure */
bool ispointer; /** declared PTR TO rather than a value */
} akbasic_Variable;
/**