Two parse handlers refusing something the documentation promises. Landing together because they are the same defect twice -- a verb's own argument shape falling through to a general path that cannot see it -- in one file, found by one program, and verified in one pass. **`GRAPHIC CLR`** is given as `GRAPHIC mode | CLR` in both docs/06-graphics.md and docs/11-verb-reference.md, and was refused: `CLR` is a verb of its own, so the generic arglist path scanned it as a command token and the expression parser answered "Expected expression or literal". `akbasic_parse_graphic()` takes it as this verb's keyword argument and emits mode 5 -- which `akbasic_cmd_graphic()` already treats as "drop the saved shapes and go back to text", so both spellings are one statement and the exec handler is untouched. The documentation was right all along; nothing in it changes. **`DATA -5`** was refused by `akbasic_parse_data()`, and only there: `READ` scans the source text directly (src/data.c) and always returned the -5 intact. So the value was right and *reaching* the statement raised -- which, since section 4 settled that `DATA` at run time is a no-op, is what a program does with every `DATA` line it walks past. A table of coordinates or velocities is full of negative numbers, which is how a game found it. The fix accepts a unary minus over a numeric literal and nothing else: `DATA -A#` is still a mistake worth naming, and `akbasic_leaf_is_literal()` keeps meaning what it says because other callers rely on it. tests/read_data.c covers both mechanisms -- reading a negative item and reaching the line after it -- with a mixed-sign table and a negative float, since the two were never the same code path. TODO.md section 9 items 7 and 8, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1130 lines
45 KiB
C
1130 lines
45 KiB
C
/**
|
|
* @file parser_commands.c
|
|
* @brief Verbs that need their own parse path rather than a plain expression.
|
|
*
|
|
* Ported from basicparser_commands.go. Two of these mutate runtime state from
|
|
* inside the parser and that is not an accident: DEF installs the function so a
|
|
* later line can call it, and FOR builds and installs the loop's environment
|
|
* before the body is ever scanned. The waitingForCommand scheme depends on the
|
|
* latter.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/parser.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "verbs.h"
|
|
|
|
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
/*
|
|
* The generic "this verb takes a comma-separated list" path, shared by the
|
|
* graphics and sound verbs. The default command path parses a *single*
|
|
* expression, which quietly turns COLOR 0, 1 into COLOR 0 with a stray 1 --
|
|
* so every verb taking more than one argument needs this rather than NULL.
|
|
*
|
|
* The verb name comes back out of the token stream rather than being passed
|
|
* in, because a parse handler is dispatched by name and the table has no
|
|
* column to carry one.
|
|
*/
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
/*
|
|
* Checked before parsing rather than after. Handing an empty token stream to
|
|
* the expression parser fails deep inside it with "peek() returned nil
|
|
* token!", which tells a program author nothing at all -- and this path is
|
|
* reached by every verb that takes a list, so the bad message was the one
|
|
* most people saw.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (akbasic_parser_peek(parser) != NULL), AKBASIC_ERR_SYNTAX,
|
|
"%s expected at least one argument", operator_->lexeme);
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX,
|
|
"%s expected at least one argument", operator_->lexeme);
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* GRAPHIC mode [, clear]
|
|
* GRAPHIC CLR
|
|
*
|
|
* `CLR` is a keyword argument rather than an expression, and the generic arglist
|
|
* path cannot see it as one: `CLR` is a verb of its own, so it scans as a
|
|
* command token and the expression parser refuses with "Expected expression or
|
|
* literal". Both `docs/06-graphics.md` and `docs/11-verb-reference.md` have given
|
|
* the form as `GRAPHIC mode | CLR` all along, so the documentation was right and
|
|
* the parser was not. TODO.md section 9 item 7.
|
|
*
|
|
* It becomes mode 5, which is what akbasic_cmd_graphic() already treats as "drop
|
|
* the saved shapes and go back to text" -- so `GRAPHIC CLR` and `GRAPHIC 5` are
|
|
* the same statement, and the exec handler needs no change.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_graphic(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND &&
|
|
strcmp(peeked->lexeme, "CLR") == 0 ) {
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist->right));
|
|
PASS(errctx, akbasic_leaf_new_literal_int(arglist->right, "5"));
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "GRAPHIC", arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, akbasic_parse_arglist(parser, dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
/*
|
|
* DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs
|
|
* instead of a comma. TO is a reserved word the argument list will not cross,
|
|
* so the pairs after the first are appended by hand and the whole thing
|
|
* flattens to one argument chain: source, x1, y1, x2, y2, ... The exec
|
|
* handler walks it in pairs and never has to know a TO was involved.
|
|
*/
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"DRAW expected a color source and a coordinate");
|
|
|
|
tail = arglist->right;
|
|
while ( tail->next != NULL ) {
|
|
tail = tail->next;
|
|
}
|
|
|
|
for ( ;; ) {
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked == NULL ||
|
|
peeked->tokentype != AKBASIC_TOK_COMMAND ||
|
|
strcmp(peeked->lexeme, "TO") != 0 ) {
|
|
break;
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX, "DRAW expected TO");
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"DRAW expected X after TO");
|
|
tail = tail->next;
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
AKBASIC_ERR_SYNTAX, "DRAW expected TO X,Y");
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"DRAW expected Y after TO X,");
|
|
tail = tail->next;
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_movspr(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *form = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
const char *formlexeme = "0";
|
|
|
|
/*
|
|
* MOVSPR is the one verb in the language whose arguments are not simply
|
|
* comma-separated. It has four forms and the separators are what tell them
|
|
* apart:
|
|
*
|
|
* MOVSPR n, x, y absolute
|
|
* MOVSPR n, +x, -y relative -- a signed coordinate, not a negative one
|
|
* MOVSPR n, d ; a polar: distance, then angle
|
|
* MOVSPR n, a # s continuous: angle, then speed
|
|
*
|
|
* So the form has to be decided here, where the tokens still exist, and
|
|
* carried to the exec handler somehow. It is carried as a synthetic integer
|
|
* literal prepended to the argument list, which flattens the whole statement
|
|
* to `form, n, a, b` -- the same trick akbasic_parse_draw uses to make a
|
|
* polyline look like a plain argument chain.
|
|
*/
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &form));
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"MOVSPR expected a sprite number");
|
|
tail = arglist->right;
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
AKBASIC_ERR_SYNTAX, "MOVSPR expected a comma after the sprite number");
|
|
|
|
/*
|
|
* A leading sign makes it relative. `+` has to be consumed here because the
|
|
* expression parser has no unary plus; `-` is left where it is, because
|
|
* unary minus is a real operator and re-parsing it would drop the sign.
|
|
*/
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL &&
|
|
(peeked->tokentype == AKBASIC_TOK_PLUS || peeked->tokentype == AKBASIC_TOK_MINUS) ) {
|
|
formlexeme = "1";
|
|
if ( peeked->tokentype == AKBASIC_TOK_PLUS ) {
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
}
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"MOVSPR expected a coordinate, a distance or an angle");
|
|
tail = tail->next;
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON) ) {
|
|
formlexeme = "2";
|
|
} else if ( akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK) ) {
|
|
formlexeme = "3";
|
|
} else {
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"MOVSPR expected `, y', `; angle' or `# speed'");
|
|
if ( formlexeme[0] == '0' ) {
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_PLUS ) {
|
|
formlexeme = "1";
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
} else if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_MINUS ) {
|
|
formlexeme = "1";
|
|
}
|
|
} else if ( akbasic_parser_peek(parser) != NULL &&
|
|
akbasic_parser_peek(parser)->tokentype == AKBASIC_TOK_PLUS ) {
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS);
|
|
}
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"MOVSPR expected a second value");
|
|
|
|
PASS(errctx, akbasic_leaf_new_literal_int(form, formlexeme));
|
|
form->next = arglist->right;
|
|
arglist->right = form;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Read an optional `WHILE`/`UNTIL` condition off a DO or a LOOP.
|
|
*
|
|
* Both ends of a DO/LOOP take the same optional clause, so both parse it the
|
|
* same way. @p kind comes back as one of the AKBASIC_LOOPCOND_* values and
|
|
* @p condition is NULL when there was no clause.
|
|
*/
|
|
static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_ASTLeaf **condition, int *kind)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
*condition = NULL;
|
|
*kind = AKBASIC_LOOPCOND_NONE;
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
if ( strcmp(peeked->lexeme, "WHILE") == 0 ) {
|
|
*kind = AKBASIC_LOOPCOND_WHILE;
|
|
} else if ( strcmp(peeked->lexeme, "UNTIL") == 0 ) {
|
|
*kind = AKBASIC_LOOPCOND_UNTIL;
|
|
} else {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
|
|
/* A loop condition is a condition, so a lone `=` in it is an equality test. */
|
|
parser->comparing = true;
|
|
PASS(errctx, akbasic_parser_expression(parser, condition));
|
|
parser->comparing = false;
|
|
FAIL_ZERO_RETURN(errctx, (*condition != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected a condition after WHILE or UNTIL");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* DO [WHILE|UNTIL (condition)]
|
|
*
|
|
* Pushes the loop's scope at parse time, exactly as FOR does and for the same
|
|
* reason: the condition leaf has to outlive the line it was written on, and an
|
|
* environment's leaf pool is what gives it somewhere to live.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
akbasic_Environment *parent = runtime->environment;
|
|
akbasic_Environment *newenv = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *condition = NULL;
|
|
int kind = AKBASIC_LOOPCOND_NONE;
|
|
int64_t firstline = parent->lineno + 1;
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
|
newenv = runtime->environment;
|
|
runtime->environment = parent;
|
|
|
|
/*
|
|
* Parsed against the parent's token stream but stored in the new scope --
|
|
* the body is scanned line by line afterwards, and the new scope must not be
|
|
* active until parsing is done.
|
|
*/
|
|
PASS(errctx, parse_loop_condition(parser, &condition, &kind));
|
|
|
|
newenv->isDoLoop = true;
|
|
newenv->loopFirstLine = firstline;
|
|
newenv->doConditionKind = kind;
|
|
if ( condition != NULL ) {
|
|
PASS(errctx, akbasic_leaf_clone(condition, &newenv->doLeafPool, &newenv->doConditionLeaf));
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "DO", NULL));
|
|
|
|
runtime->environment = newenv;
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* LOOP [WHILE|UNTIL (condition)]
|
|
*
|
|
* The condition hangs off the command leaf's `.right`, with the WHILE/UNTIL
|
|
* choice in that leaf's integer literal and the expression on its `.left`. No
|
|
* cloning: this line is still scanned when the verb runs.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_loop(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *holder = NULL;
|
|
akbasic_ASTLeaf *condition = NULL;
|
|
int kind = AKBASIC_LOOPCOND_NONE;
|
|
|
|
PASS(errctx, parse_loop_condition(parser, &condition, &kind));
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
if ( condition != NULL ) {
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &holder));
|
|
PASS(errctx, akbasic_leaf_init(holder, AKBASIC_LEAF_LITERAL_INT));
|
|
holder->literal_int = kind;
|
|
holder->left = condition;
|
|
}
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "LOOP", holder));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* ON (expression) GOTO|GOSUB (target) [, ...]
|
|
*
|
|
* Flattened to one argument chain: a marker carrying the GOSUB flag, the
|
|
* selector expression, then the targets. Same trick akbasic_parse_draw and
|
|
* akbasic_parse_movspr use, so the exec handler walks a plain list.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *marker = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
bool gosub = false;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &marker));
|
|
PASS(errctx, akbasic_leaf_init(marker, AKBASIC_LEAF_LITERAL_INT));
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &marker->next));
|
|
FAIL_ZERO_RETURN(errctx, (marker->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected ON (expression) GOTO|GOSUB (line) [, ...]");
|
|
tail = marker->next;
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)");
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
if ( strcmp(operator_->lexeme, "GOSUB") == 0 ) {
|
|
gosub = true;
|
|
} else {
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "GOTO"), AKBASIC_ERR_SYNTAX,
|
|
"Expected GOTO or GOSUB after ON (expression)");
|
|
}
|
|
marker->literal_int = (gosub ? 1 : 0);
|
|
|
|
for ( ;; ) {
|
|
PASS(errctx, akbasic_parser_expression(parser, &tail->next));
|
|
FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected a line number or label after ON ... %s",
|
|
(gosub ? "GOSUB" : "GOTO"));
|
|
tail = tail->next;
|
|
if ( !akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
arglist->right = marker;
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "ON", arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* RESUME [NEXT | (line)]
|
|
*
|
|
* `NEXT` is a COMMAND token, so the default command path -- which parses the
|
|
* rval as an expression -- cannot read it: `RESUME NEXT` came back as "Expected
|
|
* expression or literal". The word is kept as the command leaf's rval so the
|
|
* exec handler can tell the three forms apart by looking at it.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_resume(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *target = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND &&
|
|
strcmp(peeked->lexeme, "NEXT") == 0 ) {
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &target));
|
|
PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL));
|
|
} else if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
PASS(errctx, akbasic_parser_expression(parser, &target));
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "RESUME", target));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* PRINT [USING (format);] (expression)
|
|
*
|
|
* Only the USING form needs a parse handler; without it PRINT keeps the default
|
|
* path, which parses one expression. `USING` is a COMMAND token, so the default
|
|
* path could not read it -- the same reason RESUME needed one for `NEXT`.
|
|
*
|
|
* The result is a command leaf whose rval is an argument list of exactly two:
|
|
* the format and the value. A plain PRINT keeps its single-expression rval, so
|
|
* the exec handler tells them apart by looking for the list.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *right = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
|
|
/*
|
|
* `PRINT #1, expr` writes to a file channel. A C128 spells it `PRINT#1` with
|
|
* no space, which this scanner cannot produce: `PRINT#` reads as an
|
|
* identifier with an integer suffix, and a verb name carrying a suffix is
|
|
* refused (deviation 30). So the `#` is its own token and the space before
|
|
* it is required. Recorded in TODO.md section 5.
|
|
*
|
|
* The channel form is marked by a leaf named "PRINT#" holding an argument
|
|
* list of two: the channel and the value.
|
|
*/
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) {
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK);
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected PRINT #(channel), (expression)");
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"Expected a comma after PRINT #(channel)");
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected PRINT #(channel), (expression)");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT#", arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ||
|
|
strcmp(peeked->lexeme, "USING") != 0 ) {
|
|
/* The ordinary PRINT: one expression, or none at all. */
|
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
PASS(errctx, akbasic_parser_expression(parser, &right));
|
|
}
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", right));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected PRINT USING (format); (expression)");
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"Expected a semicolon between PRINT USING's format and its value");
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected PRINT USING (format); (expression)");
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* The argument list, for verbs that may be written with no arguments at all --
|
|
* bare `KEY` lists the macros, bare `RENUMBER` renumbers with its defaults.
|
|
* `akbasic_parse_arglist` insists on at least one argument, which is right for
|
|
* every other verb that uses it.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_optional_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
|
|
PASS(errctx, akbasic_parse_arglist(parser, dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, NULL));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_let(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
/*
|
|
* LET is optional in this dialect and in Commodore BASIC 7.0. Assignment is
|
|
* handled by expression evaluation, so LET parses as a bare assignment and
|
|
* its exec handler does nothing.
|
|
*/
|
|
PASS(errctx, akbasic_parser_assignment(parser, dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* LABEL and DIM share a shape: the verb, then one identifier. */
|
|
static akerr_ErrorContext *parse_verb_with_identifier(akbasic_Parser *parser, const char *verbname, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
|
"Expected identifier");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, verbname, identifier));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_label(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "LABEL", dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief `DIM NAME(SIZES)`, or `DIM NAME@ AS TYPE` / `DIM NAME@ AS PTR TO TYPE`.
|
|
*
|
|
* The `AS` clause is read here and parked on the *command* leaf: `literal_string`
|
|
* takes the type name and `literal_int` records whether `PTR TO` was written.
|
|
* Both fields are free on a command leaf, which is the same reasoning the field
|
|
* leaf uses for `.left` -- this file's header note about three defects from one
|
|
* overloaded link field is about giving a field two meanings on the *same* leaf
|
|
* type, not about using an unused one.
|
|
*
|
|
* `AS`, `PTR` and `TO` are matched by lexeme rather than promoted to keywords.
|
|
* Making them reserved words would break any existing program with a variable
|
|
* called `AS#`, and they are only meaningful in this one position.
|
|
*/
|
|
/**
|
|
* @brief `POINT P@ AT target`.
|
|
*
|
|
* Two expressions with a word between them rather than a comma, because that is
|
|
* how it reads: the verb says what is being done and `AT` says which way round
|
|
* the two operands go. `POINT P@, A@` would read equally well in either
|
|
* direction, and getting it backwards would silently point the wrong thing.
|
|
*
|
|
* The two are chained through `.next`, the argument link, so the runtime reaches
|
|
* them with akbasic_leaf_first_argument() like any other verb's operands.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_point(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *pointer = NULL;
|
|
akbasic_ASTLeaf *target = NULL;
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
akbasic_Token *word = NULL;
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &pointer));
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected POINT <pointer> AT <structure>");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AT") == 0), AKBASIC_ERR_SYNTAX,
|
|
"Expected AT after POINT <pointer>, not %s", word->lexeme);
|
|
PASS(errctx, akbasic_parser_expression(parser, &target));
|
|
|
|
pointer->next = target;
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
arglist->right = pointer;
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "POINT", arglist));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief `TYPE NAME`, whose body was already read by the prescan.
|
|
*
|
|
* Only the opening line is ever parsed: the verb jumps past its own `END TYPE`,
|
|
* so the field lines never reach the parser at all. That is deliberate -- a
|
|
* field line is a declaration, and `W#` on its own would otherwise evaluate as a
|
|
* bare identifier and quietly create a global called `W#`.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_type(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "TYPE", dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *command = NULL;
|
|
akbasic_Token *word = NULL;
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest));
|
|
command = *dest;
|
|
|
|
if ( !akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER) ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX,
|
|
"Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme);
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
if ( strcasecmp(word->lexeme, "PTR") == 0 ) {
|
|
command->literal_int = 1;
|
|
/*
|
|
* `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a
|
|
* command token rather than an identifier. Matched by lexeme here for
|
|
* the same reason AS and PTR are: these three words mean something only
|
|
* in this one position, and reserving them would break any program with
|
|
* a variable called TO#.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX,
|
|
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
}
|
|
snprintf(command->literal_string, sizeof(command->literal_string), "%s", word->lexeme);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* DEF NAME (A, ...) [= expression]
|
|
* COMMAND IDENTIFIER ARGUMENTLIST [ASSIGNMENT EXPRESSION]
|
|
*
|
|
* With an `=` the function is a single expression. Without one it is a
|
|
* multi-line subroutine whose body starts on the next line and ends at RETURN,
|
|
* so the environment is told to skip forward to that RETURN rather than execute
|
|
* the body during the definition.
|
|
*/
|
|
/**
|
|
* @brief A `DEF` parameter list, which is not an ordinary argument list.
|
|
*
|
|
* `akbasic_parser_argument_list()` parses *expressions*, and a parameter is a
|
|
* declaration -- `S@ AS RECT` is not an expression and stops that parser dead
|
|
* with "Unbalanced parenthesis". So this reads the list itself.
|
|
*
|
|
* **A structure parameter must name its type.** `@` says "a structure" without
|
|
* saying which: three primitive types fit in three suffix characters and N
|
|
* declared types do not fit in one, so `X$` fully states its contract and `X@`
|
|
* does not. A bare `X@` is refused rather than treated as "any structure",
|
|
* because that is duck typing and it puts back exactly the hole naming the type
|
|
* closes. The cost is that there are no generic functions, and it is a real one.
|
|
*
|
|
* The type name is parked on the parameter leaf in `literal_string`, with
|
|
* `literal_int` recording `PTR TO` -- the same two free fields, for the same
|
|
* reason, that `DIM ... AS` uses.
|
|
*/
|
|
static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *param = NULL;
|
|
akbasic_ASTLeaf *tail = NULL;
|
|
akbasic_Token *word = NULL;
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN),
|
|
AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF <name>");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
|
|
do {
|
|
PASS(errctx, akbasic_parser_primary(parser, ¶m));
|
|
FAIL_ZERO_RETURN(errctx,
|
|
(param != NULL &&
|
|
(param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT ||
|
|
param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT)),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"Only variable identifiers are valid arguments for DEF");
|
|
|
|
if ( param->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT ) {
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME",
|
|
param->identifier, param->identifier, param->identifier);
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX,
|
|
"Expected AS after %s, not %s", param->identifier, word->lexeme);
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
if ( strcasecmp(word->lexeme, "PTR") == 0 ) {
|
|
param->literal_int = 1;
|
|
/* `TO` is already a verb -- FOR ... TO owns it -- so it arrives
|
|
as a command token rather than an identifier. */
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX,
|
|
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
|
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
|
}
|
|
snprintf(param->literal_string, sizeof(param->literal_string), "%s", word->lexeme);
|
|
}
|
|
|
|
if ( tail == NULL ) {
|
|
arglist->right = param;
|
|
} else {
|
|
tail->next = param;
|
|
}
|
|
tail = param;
|
|
} while ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) );
|
|
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_RIGHT_PAREN),
|
|
AKBASIC_ERR_SYNTAX, "Expected ) after the argument list");
|
|
*dest = arglist;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expression = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
akbasic_FunctionDef *fndef = NULL;
|
|
size_t i = 0;
|
|
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
|
|
AKBASIC_ERR_SYNTAX, "Expected identifier");
|
|
|
|
PASS(errctx, parse_def_parameters(parser, &arglist));
|
|
|
|
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
|
|
|
|
/* Uppercase the name: verbs and function names are case-insensitive. */
|
|
FAIL_ZERO_RETURN(errctx, (strlen(identifier->identifier) < sizeof(fndef->name)),
|
|
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
|
|
for ( i = 0; i < strlen(identifier->identifier); i++ ) {
|
|
char c = identifier->identifier[i];
|
|
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
|
|
}
|
|
fndef->name[strlen(identifier->identifier)] = '\0';
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) {
|
|
PASS(errctx, akbasic_parser_expression(parser, &expression));
|
|
PASS(errctx, akbasic_leaf_clone(expression, &fndef->leafpool, &fndef->expression));
|
|
} else {
|
|
/*
|
|
* No expression: the body is the lines that follow. Record where it
|
|
* starts and skip to RETURN so the definition itself does not execute
|
|
* the body.
|
|
*/
|
|
fndef->expression = NULL;
|
|
PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "RETURN"));
|
|
}
|
|
PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist));
|
|
fndef->lineno = runtime->environment->lineno + 1;
|
|
|
|
PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0));
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "DEF", NULL));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* FOR ... TO .... [STEP ...]
|
|
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
|
*
|
|
* Sets up the loop's environment with the TO and STEP expressions and the first
|
|
* body line, then makes it the active environment. The FOR leaf itself carries
|
|
* the assignment.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Runtime *runtime = parser->runtime;
|
|
akbasic_ASTLeaf *assignment = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
akbasic_Environment *parent = runtime->environment;
|
|
akbasic_Environment *newenv = NULL;
|
|
int64_t firstline = 0;
|
|
|
|
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "TO"), AKBASIC_ERR_SYNTAX,
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
FAIL_ZERO_RETURN(errctx,
|
|
(assignment != NULL && akbasic_leaf_is_identifier(assignment->left)),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
|
|
firstline = parent->lineno + 1;
|
|
|
|
/*
|
|
* The loop body is scanned against the *parent* environment's token stream,
|
|
* so the new environment cannot become active until parsing is done. Parse
|
|
* TO and STEP first, then switch.
|
|
*/
|
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
|
newenv = runtime->environment;
|
|
runtime->environment = parent;
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &newenv->forToLeaf));
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "STEP"), AKBASIC_ERR_SYNTAX,
|
|
"Expected FOR (assignment) TO (expression) [STEP (expression)]");
|
|
PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf));
|
|
} else {
|
|
/*
|
|
* Dartmouth BASIC says not to infer a negative step: it is either given
|
|
* explicitly or assumed to be +1.
|
|
*/
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &newenv->forStepLeaf));
|
|
PASS(errctx, akbasic_leaf_new_literal_int(newenv->forStepLeaf, "1"));
|
|
}
|
|
|
|
/* A NEXT already being awaited means this is an inner loop over the same variable. */
|
|
if ( strcmp(parent->waitingForCommand, "NEXT") == 0 ) {
|
|
newenv->forNextVariable = parent->forNextVariable;
|
|
}
|
|
newenv->loopFirstLine = firstline;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "FOR", assignment));
|
|
|
|
runtime->environment = newenv;
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* READ VARNAME [, ...]
|
|
* COMMAND ARGUMENTLIST
|
|
*
|
|
* The identifier leaves are deep-copied into the environment, because the DATA
|
|
* line that fills them is parsed later and will have overwritten the per-line
|
|
* leaf pool by then.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_read(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Environment *env = parser->runtime->environment;
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
int i = 0;
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected identifier");
|
|
|
|
env->readLeafPool.next = 0;
|
|
expr = arglist->right;
|
|
for ( i = 0; i < AKBASIC_MAX_LEAVES; i++ ) {
|
|
if ( expr == NULL ) {
|
|
env->readIdentifierLeaves[i] = NULL;
|
|
continue;
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr), AKBASIC_ERR_SYNTAX,
|
|
"Expected identifier");
|
|
PASS(errctx, akbasic_leaf_clone(expr, &env->readLeafPool, &env->readIdentifierLeaves[i]));
|
|
/*
|
|
* A cloned identifier keeps its sibling chain, which for READ is the
|
|
* *next* identifier. Sever it so evaluating this leaf cannot walk into
|
|
* its sibling.
|
|
*/
|
|
env->readIdentifierLeaves[i]->next = NULL;
|
|
expr = expr->next;
|
|
}
|
|
env->readReturnLine = env->lineno + 1;
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "READ", arglist));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* DATA LITERAL [, ...]
|
|
* COMMAND ARGUMENTLIST
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_data(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected literal");
|
|
for ( expr = arglist->right; expr != NULL; expr = expr->next ) {
|
|
/*
|
|
* A negated literal counts. `DATA -5` used to be refused here -- and only
|
|
* here: `READ` scans the source text directly (src/data.c) and returned
|
|
* the -5 intact, so the value was right and reaching the *statement*
|
|
* raised "Expected literal". Since section 4 settled that DATA at run
|
|
* time is a no-op, walking past one is exactly what a program does with
|
|
* it, and a table of coordinates or velocities is full of them.
|
|
* TODO.md section 9 item 8.
|
|
*
|
|
* Only over a numeric literal: `DATA -A#` is still a mistake worth
|
|
* naming, and akbasic_leaf_is_literal() keeps meaning what it says
|
|
* because it is used elsewhere.
|
|
*/
|
|
if ( expr->leaftype == AKBASIC_LEAF_UNARY &&
|
|
expr->operator_ == AKBASIC_TOK_MINUS &&
|
|
expr->left != NULL &&
|
|
(expr->left->leaftype == AKBASIC_LEAF_LITERAL_INT ||
|
|
expr->left->leaftype == AKBASIC_LEAF_LITERAL_FLOAT) ) {
|
|
continue;
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_literal(expr), AKBASIC_ERR_SYNTAX,
|
|
"Expected literal");
|
|
}
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "DATA", arglist));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_parse_poke(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
akbasic_ASTLeaf *expr = NULL;
|
|
|
|
PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist));
|
|
FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX,
|
|
"POKE expected INTEGER, INTEGER");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
PASS(errctx, akbasic_leaf_new_command(expr, "POKE", arglist));
|
|
*dest = expr;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* IF relation THEN command [ELSE command]
|
|
* becomes BRANCH(expr=relation, left=then_command, right=else_command).
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *relation = NULL;
|
|
akbasic_ASTLeaf *then_command = NULL;
|
|
akbasic_ASTLeaf *else_command = NULL;
|
|
akbasic_ASTLeaf *branch = NULL;
|
|
akbasic_Token *operator_ = NULL;
|
|
|
|
/*
|
|
* Everything from here to the THEN is a condition, so a lone `=` in it is an
|
|
* equality test. Cleared immediately afterwards: the statement the THEN arm
|
|
* holds may well be an assignment.
|
|
*/
|
|
/*
|
|
* The whole expression, not just a relation. The reference parses one
|
|
* relation here, which is below AND and OR in the grammar chain -- so
|
|
* `IF A# = 5 AND B# = 3 THEN` stopped after the first comparison, left the
|
|
* AND unconsumed, and reported "Incomplete IF statement" against a line that
|
|
* is ordinary BASIC. A condition is an expression.
|
|
*/
|
|
parser->comparing = true;
|
|
PASS(errctx, akbasic_parser_expression(parser, &relation));
|
|
parser->comparing = false;
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
|
AKBASIC_ERR_SYNTAX, "Incomplete IF statement");
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "THEN"), AKBASIC_ERR_SYNTAX,
|
|
"Expected IF ... THEN");
|
|
|
|
PASS(errctx, akbasic_parser_command(parser, &then_command));
|
|
|
|
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
|
|
PASS(errctx, akbasic_parser_previous(parser, &operator_));
|
|
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "ELSE"), AKBASIC_ERR_SYNTAX,
|
|
"Expected IF ... THEN ... ELSE ...");
|
|
PASS(errctx, akbasic_parser_command(parser, &else_command));
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &branch));
|
|
PASS(errctx, akbasic_leaf_new_branch(branch, relation, then_command, else_command));
|
|
*dest = branch;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* INPUT "PROMPT" VARIABLE
|
|
* COMMAND EXPRESSION IDENTIFIER
|
|
*
|
|
* The prompt is hung off the identifier's .left, which is where the exec handler
|
|
* looks for it.
|
|
*/
|
|
akerr_ErrorContext *akbasic_parse_input(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *promptexpr = NULL;
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
akbasic_ASTLeaf *command = NULL;
|
|
akbasic_Token *peeked = NULL;
|
|
|
|
/*
|
|
* `INPUT #1, VAR` reads a line from a file channel rather than from the
|
|
* sink. Same spelling note as PRINT: the `#` is its own token and the space
|
|
* before it is required.
|
|
*/
|
|
peeked = akbasic_parser_peek(parser);
|
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) {
|
|
akbasic_ASTLeaf *arglist = NULL;
|
|
|
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK);
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
|
|
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
|
|
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
|
|
PASS(errctx, akbasic_parser_expression(parser, &arglist->right));
|
|
FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected INPUT #(channel), (variable)");
|
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA),
|
|
AKBASIC_ERR_SYNTAX, "Expected a comma after INPUT #(channel)");
|
|
PASS(errctx, akbasic_parser_primary(parser, &arglist->right->next));
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(arglist->right->next),
|
|
AKBASIC_ERR_SYNTAX, "Expected INPUT #(channel), (variable)");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "INPUT#", arglist));
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_parser_expression(parser, &promptexpr));
|
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
|
"Expected identifier");
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
|
PASS(errctx, akbasic_leaf_new_command(command, "INPUT", identifier));
|
|
identifier->left = promptexpr;
|
|
*dest = command;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|