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:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
180
include/akbasic/structtype.h
Normal file
180
include/akbasic/structtype.h
Normal 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_
|
||||
@@ -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_
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user