Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
225 lines
10 KiB
C
225 lines
10 KiB
C
/**
|
|
* @file parser.h
|
|
* @brief Declares the recursive-descent parser.
|
|
*
|
|
* The hierarchy is as-per "Commodore 128 Programmer's Reference Guide" page 23,
|
|
* copied verbatim from the reference because it is the specification:
|
|
*
|
|
* program -> line*
|
|
* line -> (line_number ( command | expression )) (immediate_command expression)
|
|
* command -> command (expression)
|
|
* expression -> logicalandor
|
|
* logicalandor -> logicalnot ( "OR" "AND" ) logicalnot
|
|
* logicalnot -> "NOT" relation
|
|
* relation -> subtraction* [ < <= = <> >= > ] subtraction*
|
|
* subtraction -> addition* "-" addition*
|
|
* addition -> multiplication* "+" multiplication*
|
|
* multiplication -> division* "*" division*
|
|
* division -> unary* "/" unary*
|
|
* unary -> "-" exponent
|
|
* primary -> IDENTIFIER | LITERAL_INT | LITERAL_FLOAT | LITERAL_STRING | "(" expression ")"
|
|
*/
|
|
|
|
#ifndef _AKBASIC_PARSER_H_
|
|
#define _AKBASIC_PARSER_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/grammar.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
typedef struct akbasic_Parser
|
|
{
|
|
akbasic_Runtime *runtime;
|
|
/**
|
|
* True while parsing something that cannot possibly be an assignment, which
|
|
* makes a lone `=` an equality test rather than one.
|
|
*
|
|
* The scanner reads `==` as EQUAL and a lone `=` as ASSIGNMENT, because at
|
|
* that point it cannot know whether it is looking at a statement or a
|
|
* condition. A condition is the one place the question has an answer: BASIC
|
|
* has no assignment expression, so `IF A# = 2 THEN` can only be a
|
|
* comparison -- and that is how every BASIC manual writes it. Set around the
|
|
* condition and cleared afterwards, because `FOR I# = 1 TO 5` runs through
|
|
* the same expression parser and there `=` really is an assignment.
|
|
*/
|
|
bool comparing;
|
|
} akbasic_Parser;
|
|
|
|
/**
|
|
* @brief Bind a parser to a runtime.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param runtime The runtime whose active environment holds the token stream.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` or `runtime` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_init(akbasic_Parser *obj, akbasic_Runtime *runtime);
|
|
/**
|
|
* @brief Reset the token and leaf cursors for a fresh line.
|
|
* @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_parser_zero(akbasic_Parser *obj);
|
|
|
|
/**
|
|
* @brief Parse one statement from the token stream.
|
|
*
|
|
* A line can hold more than one statement, separated by colons, so callers loop
|
|
* on akbasic_parser_is_at_end(). Leading separators are consumed here.
|
|
*
|
|
* **`*dest` may be NULL on success.** That is a line that ended in a separator,
|
|
* or one that was nothing but separators -- an empty statement, which a C128
|
|
* accepts and so does this. A caller that loops must skip a NULL leaf rather
|
|
* than handing it to the interpreter.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; NULL when the
|
|
* remaining tokens held no statement.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not parse.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_parse(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
|
|
/**
|
|
* @brief Consume a run of statement separators.
|
|
*
|
|
* Exposed because a verb with its own parse path may need to know where its
|
|
* statement ends. Returns false when nothing but separators remained.
|
|
*
|
|
* @param obj Parser to advance; a NULL or unbound parser reports false.
|
|
* @return `true` when a statement follows the separators.
|
|
*/
|
|
bool akbasic_parser_skip_separators(akbasic_Parser *obj);
|
|
|
|
/**
|
|
* @brief True when the token stream for this line is spent.
|
|
* @param obj Parser to inspect; a NULL or unbound parser is at the end.
|
|
* @return `true` when there is nothing left to parse.
|
|
*/
|
|
bool akbasic_parser_is_at_end(akbasic_Parser *obj);
|
|
|
|
/* --- Internal API: the grammar rules, reachable from src/parser_commands.c. --- */
|
|
|
|
/**
|
|
* @brief Grammar rule: a verb and its rval, or an assignment.
|
|
*
|
|
* A verb with its own parse path in the dispatch table delegates to it; anything
|
|
* else takes an expression as its rval, and a line that is not a verb at all
|
|
* falls through to assignment().
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; the leaf this rule produced.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
|
|
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Grammar rule: `IDENTIFIER = expression`, or a bare expression.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; the leaf this rule produced.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
|
|
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_assignment(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Grammar rule: the top of the expression chain.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; the leaf this rule produced.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
|
|
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_expression(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Grammar rule: a comparison between two subtractions.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; the leaf this rule produced.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
|
|
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_relation(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Grammar rule: a literal, an identifier, or a parenthesised expression.
|
|
*
|
|
* An identifier with a type suffix also consumes an array subscript list when
|
|
* one follows it.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function; the leaf this rule produced.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
|
|
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Grammar rule: comma-separated expressions joined through `.right`.
|
|
*
|
|
* With `requireparens` false and no opening paren this still builds a list,
|
|
* which is how DATA and READ take a bare comma-separated series.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param arglisttype Marks the list as function arguments or array subscripts.
|
|
* @param requireparens When true, parentheses are mandatory and unbalanced ones are an error.
|
|
* @param dest Output destination populated by the function; NULL when parens were required and absent.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When `arglisttype` is wrong or a paren is unbalanced.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_argument_list(akbasic_Parser *obj, akbasic_TokenType arglisttype, bool requireparens, akbasic_ASTLeaf **dest);
|
|
|
|
/**
|
|
* @brief Take the next leaf from the active environment's per-line pool.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_BOUNDS When the line has already used all its leaves.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_new_leaf(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
|
|
/**
|
|
* @brief Consume the next token when it is any of the given types.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param types Token types to accept.
|
|
* @param count How many entries `types` holds.
|
|
* @return `true` when a token was consumed.
|
|
*/
|
|
bool akbasic_parser_match(akbasic_Parser *obj, const akbasic_TokenType *types, int count);
|
|
|
|
/**
|
|
* @brief Consume the next token when it is of one given type.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param type Token type to accept.
|
|
* @return `true` when a token was consumed.
|
|
*/
|
|
bool akbasic_parser_match1(akbasic_Parser *obj, akbasic_TokenType type);
|
|
|
|
/**
|
|
* @brief The next token, without consuming it.
|
|
* @param obj Parser to inspect.
|
|
* @return The next token, or NULL at the end of the line.
|
|
*/
|
|
akbasic_Token *akbasic_parser_peek(akbasic_Parser *obj);
|
|
/**
|
|
* @brief The token just consumed.
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKBASIC_ERR_SYNTAX When nothing has been consumed yet.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_previous(akbasic_Parser *obj, akbasic_Token **dest);
|
|
/**
|
|
* @brief Raise a parse error naming the token the parser stopped on.
|
|
*
|
|
* Always returns an error; it never succeeds. The reference also dumps a stack
|
|
* trace here, which is not reproduced -- an interpreter library does not print
|
|
* the host's stack, and the akerr trace already carries it.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param message What was expected, appended to the position.
|
|
* @return An error context owned by the caller; never `NULL`.
|
|
* @throws AKBASIC_ERR_SYNTAX Always.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_error(akbasic_Parser *obj, const char *message);
|
|
|
|
#endif // _AKBASIC_PARSER_H_
|