Files
akbasic/src/parser_commands.c
Tachikoma b434be1901 Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.

Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.

verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.

Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.

DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.

Refs libakstdlib#26, libakstdlib#38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00

1172 lines
46 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 <akerror.h>
#include <akstdlib.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;
int cmp = 0;
peeked = akbasic_parser_peek(parser);
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
PASS(errctx, aksl_strcmp(peeked->lexeme, "CLR", &cmp));
if ( cmp == 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;
int cmp = 0;
/*
* 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 ) {
break;
}
PASS(errctx, aksl_strcmp(peeked->lexeme, "TO", &cmp));
if ( cmp != 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;
int cmp = 0;
*condition = NULL;
*kind = AKBASIC_LOOPCOND_NONE;
peeked = akbasic_parser_peek(parser);
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) {
SUCCEED_RETURN(errctx);
}
PASS(errctx, aksl_strcmp(peeked->lexeme, "WHILE", &cmp));
if ( cmp == 0 ) {
*kind = AKBASIC_LOOPCOND_WHILE;
} else {
PASS(errctx, aksl_strcmp(peeked->lexeme, "UNTIL", &cmp));
if ( cmp != 0 ) {
SUCCEED_RETURN(errctx);
}
*kind = AKBASIC_LOOPCOND_UNTIL;
}
(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;
int cmp = 0;
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_));
PASS(errctx, aksl_strcmp(operator_->lexeme, "GOSUB", &cmp));
if ( cmp == 0 ) {
gosub = true;
} else {
PASS(errctx, aksl_strcmp(operator_->lexeme, "GOTO", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, 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;
int cmp = 0;
peeked = akbasic_parser_peek(parser);
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
PASS(errctx, aksl_strcmp(peeked->lexeme, "NEXT", &cmp));
if ( cmp == 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));
}
}
/* The other two forms, taken only when the word was not NEXT -- which is
what a target already built says. */
if ( target == NULL && 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;
int cmp = 0;
bool matched = false;
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 ) {
PASS(errctx, aksl_strcmp(peeked->lexeme, "USING", &cmp));
matched = (cmp == 0);
}
if ( !matched ) {
/* 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;
int cmp = 0;
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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "AT", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 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;
int cmp = 0;
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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp));
if ( cmp == 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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 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));
}
PASS(errctx, aksl_strcpy(command->literal_string, sizeof(command->literal_string), 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;
int cmp = 0;
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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp));
if ( cmp == 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));
PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 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));
}
PASS(errctx, aksl_strcpy(param->literal_string, sizeof(param->literal_string), 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 namelen = 0;
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. */
PASS(errctx, aksl_strlen(identifier->identifier, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)),
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
for ( i = 0; i < namelen; i++ ) {
char c = identifier->identifier[i];
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
}
fndef->name[namelen] = '\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;
int cmp = 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_));
PASS(errctx, aksl_strcmp(operator_->lexeme, "TO", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, 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_));
PASS(errctx, aksl_strcmp(operator_->lexeme, "STEP", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, 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. */
PASS(errctx, aksl_strcmp(parent->waitingForCommand, "NEXT", &cmp));
if ( cmp == 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;
int cmp = 0;
/*
* 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_));
PASS(errctx, aksl_strcmp(operator_->lexeme, "THEN", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, 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_));
PASS(errctx, aksl_strcmp(operator_->lexeme, "ELSE", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, 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);
}