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>
375 lines
16 KiB
C
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_
|