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:
@@ -88,7 +88,22 @@ akerr_ErrorContext *akbasic_cmd_print(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, dest));
|
||||
PASS(errctx, akbasic_value_to_string(*dest, rendered, sizeof(rendered)));
|
||||
/*
|
||||
* A structure renders through the type table rather than through
|
||||
* akbasic_value_to_string(), which sees one slot and cannot know an instance
|
||||
* is a run of them. Split out here rather than pushed down into value.c,
|
||||
* because value.c deliberately knows nothing about the runtime.
|
||||
*/
|
||||
if ( (*dest)->valuetype == AKBASIC_TYPE_STRUCT || (*dest)->valuetype == AKBASIC_TYPE_POINTER ) {
|
||||
if ( (*dest)->structbase == NULL ) {
|
||||
snprintf(rendered, sizeof(rendered), "NOTHING");
|
||||
} else {
|
||||
PASS(errctx, akbasic_struct_to_string(obj, (*dest)->structtype, (*dest)->structbase,
|
||||
0, rendered, sizeof(rendered)));
|
||||
}
|
||||
} else {
|
||||
PASS(errctx, akbasic_value_to_string(*dest, rendered, sizeof(rendered)));
|
||||
}
|
||||
PASS(errctx, akbasic_runtime_println(obj, rendered));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -255,6 +270,57 @@ akerr_ErrorContext *akbasic_cmd_auto(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `DIM NAME@ AS TYPE`, or `DIM NAME@ AS PTR TO TYPE`.
|
||||
*
|
||||
* A value takes one run of slots sized by the type. A pointer takes one slot,
|
||||
* whatever it ends up pointing at -- it holds a reference, and a reference is
|
||||
* one value wide however large the thing on the other end is.
|
||||
*/
|
||||
static akerr_ErrorContext *dim_structure(akbasic_Runtime *obj, akbasic_ASTLeaf *expr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Variable *variable = NULL;
|
||||
int64_t sizes[1] = { 1 };
|
||||
int typeindex = -1;
|
||||
bool ispointer = (expr->literal_int != 0);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT),
|
||||
AKBASIC_ERR_TYPE,
|
||||
"DIM ... AS declares a structure, so %s must end in @",
|
||||
expr->right->identifier);
|
||||
PASS(errctx, akbasic_structtype_find(&obj->structtypes, expr->literal_string, &typeindex));
|
||||
FAIL_ZERO_RETURN(errctx, (typeindex >= 0), AKBASIC_ERR_UNDEFINED,
|
||||
"TYPE %s is not declared", expr->literal_string);
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||
"Unable to get variable for identifier %s", expr->right->identifier);
|
||||
/*
|
||||
* Re-DIMming a structure is refused rather than allowed to grow. The value
|
||||
* pool never frees, so a growing re-DIM abandons its old slots -- and a
|
||||
* pointer still aimed at them would go on reading the abandoned copy, which
|
||||
* is stale rather than wild but wrong either way. Nothing needs it.
|
||||
*/
|
||||
FAIL_NONZERO_RETURN(errctx, (variable->structtype >= 0), AKBASIC_ERR_STATE,
|
||||
"%s is already DIMmed as a structure and cannot be re-DIMmed",
|
||||
expr->right->identifier);
|
||||
|
||||
sizes[0] = (ispointer ? 1 : obj->structtypes.types[typeindex].slotcount);
|
||||
PASS(errctx, akbasic_variable_init(variable, &obj->valuepool, sizes, 1));
|
||||
variable->valuetype = AKBASIC_TYPE_STRUCT;
|
||||
variable->structtype = typeindex;
|
||||
variable->ispointer = ispointer;
|
||||
if ( ispointer ) {
|
||||
PASS(errctx, akbasic_value_zero(&variable->values[0]));
|
||||
variable->values[0].valuetype = AKBASIC_TYPE_POINTER;
|
||||
variable->values[0].structtype = typeindex;
|
||||
variable->values[0].structbase = NULL;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_dim(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -265,11 +331,26 @@ akerr_ErrorContext *akbasic_cmd_dim(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
int sizecount = 0;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL &&
|
||||
akbasic_leaf_is_identifier(expr->right)),
|
||||
AKBASIC_ERR_SYNTAX, "Expected DIM IDENTIFIER(DIMENSIONS, ...)");
|
||||
|
||||
/*
|
||||
* `DIM P@ AS RECT` takes a run of slots sized by the type, which is the same
|
||||
* thing `DIM A#(10)` does and deliberately so: a declared TYPE has a known
|
||||
* slot count, so an instance is laid out exactly as an array is and needs no
|
||||
* pool of its own.
|
||||
*/
|
||||
if ( expr->literal_string[0] != '\0' ) {
|
||||
PASS(errctx, dim_structure(obj, expr));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
(expr != NULL && expr->right != NULL && expr->right->expr != NULL &&
|
||||
(expr->right->expr != NULL &&
|
||||
expr->right->expr->leaftype == AKBASIC_LEAF_ARGUMENTLIST &&
|
||||
expr->right->expr->operator_ == AKBASIC_TOK_ARRAY_SUBSCRIPT &&
|
||||
akbasic_leaf_is_identifier(expr->right)),
|
||||
expr->right->expr->operator_ == AKBASIC_TOK_ARRAY_SUBSCRIPT),
|
||||
AKBASIC_ERR_SYNTAX, "Expected DIM IDENTIFIER(DIMENSIONS, ...)");
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &variable));
|
||||
|
||||
Reference in New Issue
Block a user