Files
akbasic/include/akbasic/structtype.h
Andrew Kesterson 6a7c8cd920 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>
2026-08-01 11:39:47 -04:00

181 lines
7.8 KiB
C

/**
* @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_