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:
255
src/runtime_struct.c
Normal file
255
src/runtime_struct.c
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @file runtime_struct.c
|
||||
* @brief The structure verbs and the field machinery behind `.` and `->`.
|
||||
*
|
||||
* **A structure is laid out exactly as an array is**, and that falls out of
|
||||
* declaring the type: `TYPE` states the fields, so an instance has a known slot
|
||||
* count and takes one contiguous run from the same value pool `DIM` already
|
||||
* draws from. A field access is then offset arithmetic against an offset
|
||||
* src/structtype.c computed before the program ran.
|
||||
*
|
||||
* Two things ride on a value to say which instance is meant -- `structtype` and
|
||||
* `structbase`, both added to akbasic_Value for this and neither shared with the
|
||||
* numeric fields. A `STRUCT` value and a `POINTER` value carry the *same*
|
||||
* reference; what differs is what assignment does with it. A structure copies
|
||||
* the slots it points at and a pointer copies the reference, which is the whole
|
||||
* of the strict-pointer rule and the reason `.` and `->` are kept apart from the
|
||||
* scanner all the way down to here.
|
||||
*
|
||||
* Copy is **deep through values and stops at pointers**, as it does for a C
|
||||
* struct holding a pointer. Because a `TYPE` cannot contain itself by value, the
|
||||
* depth of a copy is fixed by the type graph before the program starts, so no
|
||||
* copy can recurse forever and none needs a runtime depth counter. Only
|
||||
* rendering does, because a pointer can make the graph cyclic.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
#include <akbasic/structtype.h>
|
||||
|
||||
#include "verbs.h"
|
||||
|
||||
/* Most verbs answer "did something happen"; this is that answer. */
|
||||
#define SUCCEED_TRUE(__obj, __dest) \
|
||||
do { \
|
||||
*(__dest) = &(__obj)->staticTrueValue; \
|
||||
} while ( 0 )
|
||||
|
||||
akerr_ErrorContext *akbasic_struct_describe(akbasic_Runtime *obj, int typeindex, akbasic_Value *base,
|
||||
bool pointer, akbasic_Value *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in struct describe");
|
||||
PASS(errctx, akbasic_value_zero(dest));
|
||||
dest->valuetype = (pointer ? AKBASIC_TYPE_POINTER : AKBASIC_TYPE_STRUCT);
|
||||
dest->structtype = typeindex;
|
||||
dest->structbase = base;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_struct_copy(akbasic_Runtime *obj, int typeindex,
|
||||
akbasic_Value *src, akbasic_Value *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_StructType *type = NULL;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && src != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in struct copy");
|
||||
FAIL_ZERO_RETURN(errctx, (typeindex >= 0 && typeindex < obj->structtypes.count),
|
||||
AKBASIC_ERR_BOUNDS, "Structure type index %d is out of range", typeindex);
|
||||
type = &obj->structtypes.types[typeindex];
|
||||
|
||||
if ( src == dest ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
/*
|
||||
* Slot by slot rather than one memcpy of the run, because the two are not
|
||||
* the same thing: a nested pointer field must copy its reference and a
|
||||
* nested value field must copy its slots, and only walking the descriptor
|
||||
* knows which is which. A memcpy would give the right answer today and the
|
||||
* wrong one the moment a field needs anything but a byte copy -- a host
|
||||
* binding, for instance.
|
||||
*/
|
||||
for ( i = 0; i < type->slotcount; i++ ) {
|
||||
PASS(errctx, akbasic_value_clone(&src[i], &dest[i]));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve a field-access leaf to the slot it addresses.
|
||||
*
|
||||
* Walks the chain from the base outward, so `A@.POS@.X#` resolves `A@`, then
|
||||
* `POS@` within it, then `X#` within that -- each step against a type the
|
||||
* declaration already fixed, which is why a misspelled field can be refused by
|
||||
* name at all.
|
||||
*/
|
||||
akerr_ErrorContext *akbasic_struct_resolve(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
akbasic_StructField **fielddest, akbasic_Value **slotdest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Value *basevalue = NULL;
|
||||
akbasic_StructField *field = NULL;
|
||||
akbasic_Value *slot = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && slotdest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in struct resolve");
|
||||
FAIL_ZERO_RETURN(errctx, (expr->leaftype == AKBASIC_LEAF_FIELD), AKBASIC_ERR_SYNTAX,
|
||||
"Not a field access");
|
||||
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, &basevalue));
|
||||
|
||||
/*
|
||||
* The strict-pointer rule, and the only place it is enforced. `.` requires a
|
||||
* structure and `->` requires a pointer to one; neither stands in for the
|
||||
* other, so a reader always knows from the spelling whether the thing on the
|
||||
* left is a copy or a reference to somebody else's data.
|
||||
*/
|
||||
if ( expr->operator_ == AKBASIC_TOK_ARROW ) {
|
||||
FAIL_ZERO_RETURN(errctx, (basevalue->valuetype == AKBASIC_TYPE_POINTER),
|
||||
AKBASIC_ERR_TYPE,
|
||||
"-> needs a pointer on its left; use . to reach a field of a structure");
|
||||
FAIL_ZERO_RETURN(errctx, (basevalue->structbase != NULL), AKBASIC_ERR_VALUE,
|
||||
"This pointer is not pointing at anything yet; POINT it AT a structure first");
|
||||
} else {
|
||||
FAIL_ZERO_RETURN(errctx, (basevalue->valuetype == AKBASIC_TYPE_STRUCT),
|
||||
AKBASIC_ERR_TYPE,
|
||||
(basevalue->valuetype == AKBASIC_TYPE_POINTER
|
||||
? "'.' needs a structure on its left; use -> to reach a field through a pointer"
|
||||
: "'.' needs a structure on its left"));
|
||||
FAIL_ZERO_RETURN(errctx, (basevalue->structbase != NULL), AKBASIC_ERR_VALUE,
|
||||
"This structure has no storage; DIM it AS a type first");
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_structtype_field(&obj->structtypes, basevalue->structtype,
|
||||
expr->identifier, &field));
|
||||
slot = basevalue->structbase + field->offset;
|
||||
if ( fielddest != NULL ) {
|
||||
*fielddest = field;
|
||||
}
|
||||
*slotdest = slot;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_struct_evaluate_field(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_StructField *field = NULL;
|
||||
akbasic_Value *slot = NULL;
|
||||
akbasic_Value *out = NULL;
|
||||
|
||||
PASS(errctx, akbasic_struct_resolve(obj, expr, &field, &slot));
|
||||
|
||||
/*
|
||||
* A nested structure evaluates to a description of where it lives, not to a
|
||||
* copy of it: `A@.POS@.X#` has to reach through it, and `B@ = A@.POS@` has
|
||||
* to know how many slots to copy. The copy happens in assignment, which is
|
||||
* the one place that knows a copy was asked for.
|
||||
*/
|
||||
if ( field->kind == AKBASIC_FIELD_STRUCT ) {
|
||||
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
||||
PASS(errctx, akbasic_struct_describe(obj, field->typeindex, slot, false, out));
|
||||
*dest = out;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
*dest = slot;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Render a structure the way PRINT does.
|
||||
*
|
||||
* `RECT(W#=3, H#=4)`. Bounded by depth because a pointer can make the graph
|
||||
* cyclic -- copy cannot follow one, but rendering must, or a linked list would
|
||||
* print as an unhelpful `NODE(VAL#=1, NEXT@=...)` and stop being worth printing.
|
||||
*/
|
||||
akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex, akbasic_Value *base,
|
||||
int depth, char *dest, size_t len)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
char rendered[AKBASIC_MAX_STRING_LENGTH];
|
||||
akbasic_StructType *type = NULL;
|
||||
size_t used = 0;
|
||||
int written = 0;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in struct to_string");
|
||||
FAIL_ZERO_RETURN(errctx, (typeindex >= 0 && typeindex < obj->structtypes.count),
|
||||
AKBASIC_ERR_BOUNDS, "Structure type index %d is out of range", typeindex);
|
||||
type = &obj->structtypes.types[typeindex];
|
||||
|
||||
if ( depth >= AKBASIC_MAX_STRUCT_DEPTH ) {
|
||||
snprintf(dest, len, "%s(...)", type->name);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
written = snprintf(dest, len, "%s(", type->name);
|
||||
used = (written > 0 ? (size_t)written : 0);
|
||||
|
||||
for ( i = 0; i < type->fieldcount && used + 1 < len; i++ ) {
|
||||
akbasic_StructField *field = &type->fields[i];
|
||||
akbasic_Value *slot = base + field->offset;
|
||||
|
||||
switch ( field->kind ) {
|
||||
case AKBASIC_FIELD_STRUCT:
|
||||
PASS(errctx, akbasic_struct_to_string(obj, field->typeindex, slot, depth + 1,
|
||||
rendered, sizeof(rendered)));
|
||||
break;
|
||||
case AKBASIC_FIELD_POINTER:
|
||||
if ( slot->structbase == NULL ) {
|
||||
snprintf(rendered, sizeof(rendered), "NOTHING");
|
||||
} else {
|
||||
PASS(errctx, akbasic_struct_to_string(obj, slot->structtype, slot->structbase,
|
||||
depth + 1, rendered, sizeof(rendered)));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
PASS(errctx, akbasic_value_to_string(slot, rendered, sizeof(rendered)));
|
||||
break;
|
||||
}
|
||||
written = snprintf(dest + used, len - used, "%s%s=%s",
|
||||
(i == 0 ? "" : ", "), field->name, rendered);
|
||||
if ( written < 0 || (size_t)written >= len - used ) {
|
||||
break;
|
||||
}
|
||||
used += (size_t)written;
|
||||
}
|
||||
if ( used + 1 < len ) {
|
||||
snprintf(dest + used, len - used, ")");
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- TYPE --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_type(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int block = -1;
|
||||
|
||||
(void)expr; (void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in TYPE");
|
||||
|
||||
/*
|
||||
* The declaration was read before the program started; what is left to do at
|
||||
* run time is to *not* execute the block. Its field lines are declarations,
|
||||
* and letting them run would evaluate each field name as a bare identifier
|
||||
* and quietly create a global of that name -- so this jumps past END TYPE.
|
||||
*/
|
||||
PASS(errctx, akbasic_structtype_block_at(&obj->structtypes, obj->environment->lineno, &block));
|
||||
FAIL_ZERO_RETURN(errctx, (block >= 0), AKBASIC_ERR_STATE,
|
||||
"TYPE was not read by the prescan; this line is not the start of a block");
|
||||
obj->environment->nextline = obj->structtypes.types[block].lastline + 1;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user