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:
66
src/parser.c
66
src/parser.c
@@ -249,10 +249,17 @@ akerr_ErrorContext *akbasic_parser_assignment(akbasic_Parser *obj, akbasic_ASTLe
|
||||
akbasic_ASTLeaf *right = NULL;
|
||||
|
||||
PASS(errctx, akbasic_parser_expression(obj, &identifier));
|
||||
/*
|
||||
* A field access is assignable too, and it is not an identifier: the
|
||||
* name on the left of the = is reached by walking a chain rather than by
|
||||
* looking one name up. Both spellings continue into the = below.
|
||||
*/
|
||||
if ( identifier == NULL ||
|
||||
(identifier->leaftype != AKBASIC_LEAF_IDENTIFIER_INT &&
|
||||
identifier->leaftype != AKBASIC_LEAF_IDENTIFIER_FLOAT &&
|
||||
identifier->leaftype != AKBASIC_LEAF_IDENTIFIER_STRING) ) {
|
||||
identifier->leaftype != AKBASIC_LEAF_IDENTIFIER_STRING &&
|
||||
identifier->leaftype != AKBASIC_LEAF_IDENTIFIER_STRUCT &&
|
||||
identifier->leaftype != AKBASIC_LEAF_FIELD) ) {
|
||||
*dest = identifier;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -623,19 +630,67 @@ static akerr_ErrorContext *function_call(akbasic_Parser *obj, akbasic_ASTLeaf **
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Consume a run of `.field` and `->field` after a primary.
|
||||
*
|
||||
* Left-associative, so `A@.B@.C#` reads as `(A@.B@).C#` and each step's base is
|
||||
* the leaf before it. The two operators are kept apart all the way to the
|
||||
* runtime rather than folded into one "field access", because which one was
|
||||
* written is the whole of the strict-pointer rule: `.` requires a structure and
|
||||
* `->` requires a pointer to one, and getting it wrong is an error rather than a
|
||||
* convenience the interpreter silently absorbs.
|
||||
*/
|
||||
static akerr_ErrorContext *parse_field_chain(akbasic_Parser *obj, akbasic_ASTLeaf **expr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
static const akbasic_TokenType ACCESSORS[] = { AKBASIC_TOK_DOT, AKBASIC_TOK_ARROW };
|
||||
/*
|
||||
* A field name carries its own type suffix, exactly as a variable does, so
|
||||
* the four suffixed identifier tokens are the whole set. A bare identifier
|
||||
* is not among them: that spelling is a label everywhere else and a field
|
||||
* with no suffix would have no type.
|
||||
*/
|
||||
static const akbasic_TokenType FIELDNAMES[] = {
|
||||
AKBASIC_TOK_IDENTIFIER_INT, AKBASIC_TOK_IDENTIFIER_FLOAT,
|
||||
AKBASIC_TOK_IDENTIFIER_STRING, AKBASIC_TOK_IDENTIFIER_STRUCT
|
||||
};
|
||||
akbasic_ASTLeaf *field = NULL;
|
||||
akbasic_Token *accessor = NULL;
|
||||
akbasic_Token *name = NULL;
|
||||
akbasic_TokenType op = AKBASIC_TOK_DOT;
|
||||
|
||||
while ( akbasic_parser_match(obj, ACCESSORS, 2) ) {
|
||||
PASS(errctx, akbasic_parser_previous(obj, &accessor));
|
||||
op = accessor->tokentype;
|
||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match(obj, FIELDNAMES, 4), AKBASIC_ERR_SYNTAX,
|
||||
"Expected a field name after %s", (op == AKBASIC_TOK_ARROW ? "->" : "."));
|
||||
PASS(errctx, akbasic_parser_previous(obj, &name));
|
||||
PASS(errctx, akbasic_parser_new_leaf(obj, &field));
|
||||
PASS(errctx, akbasic_leaf_new_field(field, *expr, name->lexeme, op));
|
||||
/*
|
||||
* A field may itself be an array element -- `E@.DROPS#(2)` -- and the
|
||||
* subscript list belongs to the field, not to the base, so it is
|
||||
* collected here and onto the field leaf's own `.expr`.
|
||||
*/
|
||||
PASS(errctx, akbasic_parser_argument_list(obj, AKBASIC_TOK_ARRAY_SUBSCRIPT, true, &field->expr));
|
||||
*expr = field;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
static const akbasic_TokenType PRIMARIES[] = {
|
||||
AKBASIC_TOK_LITERAL_INT, AKBASIC_TOK_LITERAL_FLOAT, AKBASIC_TOK_LITERAL_STRING,
|
||||
AKBASIC_TOK_IDENTIFIER, AKBASIC_TOK_IDENTIFIER_STRING, AKBASIC_TOK_IDENTIFIER_FLOAT,
|
||||
AKBASIC_TOK_IDENTIFIER_INT, AKBASIC_TOK_FUNCTION
|
||||
AKBASIC_TOK_IDENTIFIER_INT, AKBASIC_TOK_FUNCTION, AKBASIC_TOK_IDENTIFIER_STRUCT
|
||||
};
|
||||
akbasic_ASTLeaf *expr = NULL;
|
||||
akbasic_ASTLeaf *groupexpr = NULL;
|
||||
akbasic_Token *previous = NULL;
|
||||
|
||||
if ( akbasic_parser_match(obj, PRIMARIES, 8) ) {
|
||||
if ( akbasic_parser_match(obj, PRIMARIES, 9) ) {
|
||||
PASS(errctx, akbasic_parser_previous(obj, &previous));
|
||||
PASS(errctx, akbasic_parser_new_leaf(obj, &expr));
|
||||
switch ( previous->tokentype ) {
|
||||
@@ -660,6 +715,10 @@ akerr_ErrorContext *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf
|
||||
PASS(errctx, akbasic_leaf_new_identifier(expr, AKBASIC_LEAF_IDENTIFIER_STRING, previous->lexeme));
|
||||
PASS(errctx, akbasic_parser_argument_list(obj, AKBASIC_TOK_ARRAY_SUBSCRIPT, true, &expr->expr));
|
||||
break;
|
||||
case AKBASIC_TOK_IDENTIFIER_STRUCT:
|
||||
PASS(errctx, akbasic_leaf_new_identifier(expr, AKBASIC_LEAF_IDENTIFIER_STRUCT, previous->lexeme));
|
||||
PASS(errctx, akbasic_parser_argument_list(obj, AKBASIC_TOK_ARRAY_SUBSCRIPT, true, &expr->expr));
|
||||
break;
|
||||
case AKBASIC_TOK_FUNCTION:
|
||||
case AKBASIC_TOK_IDENTIFIER:
|
||||
PASS(errctx, akbasic_leaf_new_identifier(expr, AKBASIC_LEAF_IDENTIFIER, previous->lexeme));
|
||||
@@ -667,6 +726,7 @@ akerr_ErrorContext *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf
|
||||
default:
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX, "Invalid literal type, command or function name");
|
||||
}
|
||||
PASS(errctx, parse_field_chain(obj, &expr));
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user