Files
akbasic/src/parser_commands.c
Andrew Kesterson d5f3c3ef5b Accept GRAPHIC CLR and a negative DATA item
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>
2026-08-02 00:18:53 -04:00

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, &param));
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);
}