The token has existed since the scanner was written and nothing read it, so 10 PRINT A$ : REM ... was a parse error. Leading separators are consumed before each statement and an empty statement yields a NULL leaf rather than an error, so a trailing colon and a run of them are both legal. BASIC 7.0 scopes everything after THEN to the condition, which the reference had no opinion about because it never got here. The rule is not "skip when false": the rest of the line belongs to whichever arm was written last. A whole FOR/NEXT on one line still does not loop -- block skipping works by source line. Recorded in TODO.md as what group A has to fix first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
212 lines
9.6 KiB
C
212 lines
9.6 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;
|
|
} 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_
|