Files
akbasic/include/akbasic/grammar.h
Tachikoma 4f86a9ca44 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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 11:39:47 -04:00

375 lines
16 KiB
C

/**
* @file grammar.h
* @brief Declares the token and AST leaf types the scanner and parser share.
*
* The reference splits these across basicscanner.go (BasicTokenType),
* basicparser.go (BasicToken) and basicgrammar.go (BasicASTLeafType,
* BasicASTLeaf). They are one header here because a leaf's operator *is* a token
* type and separating them buys nothing in C. The numeric values are preserved
* from the reference so a debugging session against either implementation reads
* the same.
*/
#ifndef _AKBASIC_GRAMMAR_H_
#define _AKBASIC_GRAMMAR_H_
#include <akerror.h>
#include <akbasic/types.h>
typedef enum
{
AKBASIC_TOK_UNDEFINED = 0,
AKBASIC_TOK_EQUAL, /* 1 */
AKBASIC_TOK_LESS_THAN, /* 2 */
AKBASIC_TOK_LESS_THAN_EQUAL, /* 3 */
AKBASIC_TOK_GREATER_THAN, /* 4 */
AKBASIC_TOK_GREATER_THAN_EQUAL, /* 5 */
AKBASIC_TOK_COMMA, /* 6 */
AKBASIC_TOK_HASH, /* 7 */
AKBASIC_TOK_NOT_EQUAL, /* 8 */
AKBASIC_TOK_LEFT_PAREN, /* 9 */
AKBASIC_TOK_RIGHT_PAREN, /* 10 */
AKBASIC_TOK_PLUS, /* 11 */
AKBASIC_TOK_MINUS, /* 12 */
AKBASIC_TOK_LEFT_SLASH, /* 13 */
AKBASIC_TOK_STAR, /* 14 */
AKBASIC_TOK_CARAT, /* 15 */
AKBASIC_TOK_LITERAL_STRING, /* 16 */
AKBASIC_TOK_LITERAL_INT, /* 17 */
AKBASIC_TOK_LITERAL_FLOAT, /* 18 */
AKBASIC_TOK_IDENTIFIER, /* 19 */
AKBASIC_TOK_IDENTIFIER_STRING, /* 20 */
AKBASIC_TOK_IDENTIFIER_FLOAT, /* 21 */
AKBASIC_TOK_IDENTIFIER_INT, /* 22 */
AKBASIC_TOK_COLON, /* 23 */
AKBASIC_TOK_AND, /* 24 */
AKBASIC_TOK_NOT, /* 25 */
AKBASIC_TOK_OR, /* 26 */
AKBASIC_TOK_REM, /* 27 */
AKBASIC_TOK_EOL, /* 28 */
AKBASIC_TOK_EOF, /* 29 */
AKBASIC_TOK_LINE_NUMBER, /* 30 -- an integer literal in token position 0 */
AKBASIC_TOK_COMMAND, /* 31 */
AKBASIC_TOK_COMMAND_IMMEDIATE, /* 32 */
AKBASIC_TOK_FUNCTION, /* 33 */
AKBASIC_TOK_ASSIGNMENT, /* 34 */
AKBASIC_TOK_LEFT_SQUAREBRACKET, /* 35 */
AKBASIC_TOK_RIGHT_SQUAREBRACKET, /* 36 */
AKBASIC_TOK_ARRAY_SUBSCRIPT, /* 37 */
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
* the numbering is part of this enum's published shape.
*/
AKBASIC_TOK_SEMICOLON, /* 41 */
AKBASIC_TOK_HASHMARK /* 42 */
} akbasic_TokenType;
typedef enum
{
AKBASIC_LEAF_UNDEFINED = 0,
AKBASIC_LEAF_LITERAL_INT, /* 1 */
AKBASIC_LEAF_LITERAL_FLOAT, /* 2 */
AKBASIC_LEAF_LITERAL_STRING, /* 3 */
AKBASIC_LEAF_IDENTIFIER, /* 4 */
AKBASIC_LEAF_IDENTIFIER_INT, /* 5 */
AKBASIC_LEAF_IDENTIFIER_FLOAT, /* 6 */
AKBASIC_LEAF_IDENTIFIER_STRING, /* 7 */
AKBASIC_LEAF_UNARY, /* 8 */
AKBASIC_LEAF_BINARY, /* 9 */
AKBASIC_LEAF_GROUPING, /* 10 */
AKBASIC_LEAF_EQUALITY, /* 11 */
AKBASIC_LEAF_COMPARISON, /* 12 */
AKBASIC_LEAF_TERM, /* 13 */
AKBASIC_LEAF_PRIMARY, /* 14 */
AKBASIC_LEAF_COMMAND, /* 15 */
AKBASIC_LEAF_COMMAND_IMMEDIATE, /* 16 */
AKBASIC_LEAF_FUNCTION, /* 17 */
AKBASIC_LEAF_BRANCH, /* 18 */
AKBASIC_LEAF_ARGUMENTLIST, /* 19 */
AKBASIC_LEAF_IDENTIFIER_STRUCT, /* 20 */
AKBASIC_LEAF_FIELD /* 21 -- `base . name`; base on .left */
} akbasic_LeafType;
typedef struct
{
akbasic_TokenType tokentype;
int64_t lineno;
char lexeme[AKBASIC_MAX_LINE_LENGTH];
} akbasic_Token;
typedef struct akbasic_ASTLeaf
{
akbasic_LeafType leaftype;
int64_t literal_int;
char literal_string[AKBASIC_MAX_STRING_LENGTH];
double literal_float;
char identifier[AKBASIC_MAX_STRING_LENGTH];
akbasic_TokenType operator_;
struct akbasic_ASTLeaf *parent;
struct akbasic_ASTLeaf *left;
struct akbasic_ASTLeaf *right;
struct akbasic_ASTLeaf *expr;
/**
* The next argument or subscript in a list, and nothing else.
*
* **This field exists because reusing `.right` for it was a bug.** The
* reference chains an argument list through each argument's `.right`
* (`basicparser.go`'s argumentList), which is also where a unary leaf keeps
* its operand, where a binary leaf keeps its right-hand side, and where an
* identifier used to keep its subscript list. The three meanings were
* indistinguishable, and every one of them produced a wrong answer:
*
* ABS(-9) counted as two arguments, refused
* MOD(A#, B# + 12) counted as three, refused
* MOD(-7, 3) the second argument overwrote the first's operand
*
* -- TODO.md section 6 item 13, and the "array references in parameter
* lists" item in section 4, which turned out to be the same defect twice.
* An argument list's *head* is still the list leaf's `.right`; only the
* sibling chain moved here.
*/
struct akbasic_ASTLeaf *next;
} akbasic_ASTLeaf;
/**
* @brief A pool of leaves a deep clone can draw from.
*
* The reference's BasicASTLeaf.clone() recurses and allocates. Here the caller
* supplies storage and a deep clone that would exceed it fails rather than
* growing without bound.
*/
typedef struct
{
int next;
int capacity;
akbasic_ASTLeaf *leaves;
} akbasic_LeafPool;
/**
* @brief Reset a token to the undefined state.
* @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_token_init(akbasic_Token *obj);
/**
* @brief Reset a leaf, clearing every payload and link.
* @param obj Object to initialize, inspect, or modify.
* @param leaftype Type to stamp on the leaf.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype);
/**
* @brief Deep-copy a leaf and everything hanging off it into pool storage.
* @param self Source leaf.
* @param pool Storage the copies are drawn from.
* @param dest Output destination populated by the function.
* @throws AKBASIC_ERR_BOUNDS When the pool runs out of leaves.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_clone(akbasic_ASTLeaf *self, akbasic_LeafPool *pool, akbasic_ASTLeaf **dest);
/**
* @brief The first argument of a function-call leaf.
* @param self Leaf to inspect.
* @return The first argument, or NULL when the leaf carries no argument list.
*/
akbasic_ASTLeaf *akbasic_leaf_first_argument(akbasic_ASTLeaf *self);
/**
* @brief The first subscript of an array-reference leaf.
* @param self Leaf to inspect.
* @return The first subscript, or NULL when the leaf carries no subscript list.
*/
akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self);
/**
* @brief True when a leaf is any of the four identifier kinds.
* @param self Leaf to inspect; NULL is not an identifier.
* @return `true` when the leaf names a variable or a label.
*/
bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self);
/**
* @brief The value type an identifier leaf's suffix asks for.
*
* `A#` is an integer, `A%` a float, `A$` a string. A leaf that is not an
* identifier, or one with no suffix -- a label -- reports
* #AKBASIC_TYPE_UNDEFINED.
*
* @param self Leaf to inspect; NULL reports undefined.
* @return The type, or #AKBASIC_TYPE_UNDEFINED.
*/
akbasic_Type akbasic_leaf_identifier_type(akbasic_ASTLeaf *self);
/**
* @brief True when a leaf is an integer, float or string literal.
* @param self Leaf to inspect; NULL is not a literal.
* @return `true` when the leaf carries a literal value.
*/
bool akbasic_leaf_is_literal(akbasic_ASTLeaf *self);
/**
* @brief Build a comparison leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param left Left operand.
* @param op Comparison operator; must be one of the six relational tokens.
* @param right Right operand.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `left` or `right` is NULL.
* @throws AKBASIC_ERR_SYNTAX When `op` is not a comparison operator.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_comparison(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right);
/**
* @brief Build a binary-operator leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param left Left operand.
* @param op Operator token.
* @param right Right operand.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `left` or `right` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_binary(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right);
/**
* @brief Build a unary-operator leaf.
*
* **The operand hangs off `.left`, not `.right`, and that is deliberate.** The
* reference puts it on `.right` (basicgrammar.go's newUnary), which is also
* where an argument list chains its arguments -- so `ABS(-9)` counted as two
* arguments and was refused, and a unary argument followed by a comma had its
* operand overwritten by the next argument. TODO.md section 6 item 13. A unary
* leaf has no other use for `.left`, so moving it there separates the two
* meanings for the cost of one field nobody was using.
*
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param op Operator token.
* @param operand The expression the operator applies to.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `operand` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *operand);
/**
* @brief Build a function-call leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param fname Function name as written; case is preserved for the error message.
* @param right Argument list, or NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_function(akbasic_ASTLeaf *obj, const char *fname, akbasic_ASTLeaf *right);
/**
* @brief Build a verb leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param cmdname Verb name as written.
* @param right The verb's rval, or NULL for a verb that takes none.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right);
/**
* @brief Build a leaf for a verb a REPL may run without a line number.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param cmdname Verb name as written.
* @param right The verb's rval, or NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_immediate_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right);
/**
* @brief Build a branch leaf, which is what IF parses to.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param expr Condition, evaluated for a BASIC boolean.
* @param trueleaf Taken when the condition is true.
* @param falseleaf Taken when it is false; may be NULL, as ELSE is optional.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `expr` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_branch(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr, akbasic_ASTLeaf *trueleaf, akbasic_ASTLeaf *falseleaf);
/**
* @brief Build a parenthesised-expression leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param expr The grouped expression.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `expr` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_grouping(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr);
/**
* @brief Build an integer literal leaf from its lexeme.
*
* A `0x` prefix selects base 16 and a bare leading `0` selects base 8, so `010`
* is 8 and `08` will not parse. Commodore BASIC has no octal literals; this is
* reproduced from the reference and filed as TODO.md section 6 item 10.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme Digits as the scanner captured them.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the lexeme is empty or not a valid number in its base.
* @throws ERANGE When the value does not fit in an int64_t.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build a float literal leaf from its lexeme.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme Digits as the scanner captured them.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the lexeme is not a valid float.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_float(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build a string literal leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme String contents, without the surrounding quotes.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the literal exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_string(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build an identifier leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param leaftype Which identifier type the suffix selected.
* @param lexeme Identifier name, including its type suffix.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
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.
*
* Prefix form, so `1 + 2` reads `(+ 1 2)`. Note that an assignment renders with
* an empty operator -- the reference's operator table has a case for `=` meaning
* equality but none for assignment -- so `A# = 1` reads `( A# 1)`.
*
* @param self Leaf to render.
* @param dest Output destination populated by the function.
* @param len Size of `dest`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `self` or `dest` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `len` is zero.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, size_t len);
#endif // _AKBASIC_GRAMMAR_H_