/**
 * @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"

/* Shared by akbasic_parse_for() and akbasic_parse_do(); defined after akbasic_parse_def(). */
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr);

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;
    akbasic_Token *peeked = NULL;
    int kind = AKBASIC_LOOPCOND_NONE;
    int64_t firstline = parent->lineno + 1;
    int cmp = 0;

    /*
     * DO EACH <variable> IN <generator call> ... LOOP. Mutually exclusive with
     * DO WHILE/UNTIL on the same DO, so this is checked first and returns
     * before any of the WHILE/UNTIL machinery runs.
     */
    peeked = akbasic_parser_peek(parser);
    if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
	PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
	if ( cmp == 0 ) {
	    akbasic_ASTLeaf *var = NULL;
	    akbasic_ASTLeaf *callexpr = NULL;

	    (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);

	    PASS(errctx, akbasic_runtime_new_environment(runtime));
	    newenv = runtime->environment;
	    runtime->environment = parent;

	    PASS(errctx, parse_each_clause(parser, &var, &callexpr));

	    /*
	     * Same guard as akbasic_parse_for()'s EACH branch, with the likely
	     * mistake named: a condition belongs on the LOOP, where it is
	     * checked against each emitted value, not here on the DO.
	     */
	    peeked = akbasic_parser_peek(parser);
	    FAIL_NONZERO_RETURN(errctx,
				(peeked != NULL &&
				 peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
				 peeked->tokentype != AKBASIC_TOK_COLON),
				AKBASIC_ERR_SYNTAX,
				"DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here");

	    newenv->isDoLoop = true;
	    newenv->isEachLoop = true;
	    newenv->loopFirstLine = firstline;
	    /* See akbasic_parse_for()'s EACH branch for why this is not cloned. */
	    newenv->forToLeaf = callexpr;

	    PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
	    PASS(errctx, akbasic_leaf_new_command(expr, "DO", var));

	    runtime->environment = newenv;
	    *dest = expr;
	    SUCCEED_RETURN(errctx);
	}
    }

    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);
}

/*
 * GEN NAME(parameters) ... END GEN
 *
 * A DEF that yields more than once, in the same shape a multi-line DEF is:
 * the header is parsed, the parameter list is read with parse_def_parameters()
 * (GEN and DEF share the functions table -- TODO.md's namespace decision for
 * this feature -- so a name cannot be both), and the body is skipped on this,
 * the definitional pass, by arming akbasic_environment_wait_for_command() for
 * "END GEN" instead of "RETURN". It only really executes when a FOR EACH/DO
 * EACH invokes it through akbasic_runtime_generator_invoke(), which sets
 * `nextline` to `fndef->lineno` directly and never runs this line again.
 *
 * There is no single-expression form: a GEN with nothing to loop over is just
 * a DEF, and EMIT already needs a body to sit in.
 */
akerr_ErrorContext *akbasic_parse_gen(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Runtime *runtime = parser->runtime;
    akbasic_ASTLeaf *identifier = NULL;
    akbasic_ASTLeaf *arglist = 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, functions and generators are all 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';

    fndef->expression = NULL;
    fndef->isGenerator = true;
    PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "END GEN"));
    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, "GEN", NULL));
    *dest = command;
    SUCCEED_RETURN(errctx);
}

/*
 * END [GEN]
 *
 * Bare END finishes the program, the default command path's job before this
 * handler existed. `END GEN` is a GEN body's own closing verb, and it is
 * never scanned as one token -- GEN follows END as an ordinary COMMAND token
 * on the same line -- so this is what tells the two apart and builds the
 * compound leaf akbasic_cmd_end_gen dispatches on, the same trick
 * akbasic_parse_print() uses for `PRINT #`.
 */
akerr_ErrorContext *akbasic_parse_end(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *expr = NULL;
    akbasic_ASTLeaf *right = 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, "GEN", &cmp));
	if ( cmp == 0 ) {
	    (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
	    PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
	    PASS(errctx, akbasic_leaf_new_command(expr, "END GEN", NULL));
	    *dest = expr;
	    SUCCEED_RETURN(errctx);
	}
    }

    /* A plain END, matching what the default command path used to do. */
    if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
	 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, "END", right));
    *dest = expr;
    SUCCEED_RETURN(errctx);
}

/*
 * EACH <variable> IN <generator call>
 *
 * Shared by akbasic_parse_for() and akbasic_parse_do(), both of which consume
 * the leading EACH themselves (it is what tells them this is an EACH loop
 * rather than their ordinary form) before calling this for the rest.
 */
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr)
{
    PREPARE_ERROR(errctx);
    akbasic_Token *word = NULL;
    int cmp = 0;

    PASS(errctx, akbasic_parser_expression(parser, var));
    FAIL_ZERO_RETURN(errctx, (*var != NULL && akbasic_leaf_is_identifier(*var)), AKBASIC_ERR_SYNTAX,
		     "Expected EACH (variable) IN (generator call)");

    FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX,
		     "Expected IN after EACH (variable)");
    PASS(errctx, akbasic_parser_previous(parser, &word));
    PASS(errctx, aksl_strcmp(word->lexeme, "IN", &cmp));
    FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IN after EACH (variable)");

    PASS(errctx, akbasic_parser_expression(parser, callexpr));
    FAIL_ZERO_RETURN(errctx, (*callexpr != NULL && (*callexpr)->leaftype == AKBASIC_LEAF_FUNCTION),
		     AKBASIC_ERR_SYNTAX, "Expected a generator call after IN");
    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_Token *peeked = NULL;
    akbasic_Environment *parent = runtime->environment;
    akbasic_Environment *newenv = NULL;
    int64_t firstline = 0;
    int cmp = 0;

    /*
     * FOR EACH <variable> IN <generator call>. Checked before the leaf right of
     * FOR is required to be an assignment, because EACH is the one other thing
     * that leaf is allowed to be.
     */
    peeked = akbasic_parser_peek(parser);
    if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
	PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
	if ( cmp == 0 ) {
	    akbasic_ASTLeaf *var = NULL;
	    akbasic_ASTLeaf *callexpr = NULL;

	    (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
	    firstline = parent->lineno + 1;

	    /*
	     * Pushed the same way a plain FOR's environment is: while the
	     * parent is still active, so the call expression's identifiers
	     * resolve in the caller's scope rather than the loop's own.
	     */
	    PASS(errctx, akbasic_runtime_new_environment(runtime));
	    newenv = runtime->environment;
	    runtime->environment = parent;

	    PASS(errctx, parse_each_clause(parser, &var, &callexpr));

	    /*
	     * Nothing may follow the generator call but another statement.
	     * Without this, a stray clause sits unparsed on the line and only
	     * blows up after the whole loop has run, when the parent scope
	     * resumes the line mid-statement -- an error at the loop's end
	     * pointing at its beginning.
	     */
	    peeked = akbasic_parser_peek(parser);
	    FAIL_NONZERO_RETURN(errctx,
				(peeked != NULL &&
				 peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
				 peeked->tokentype != AKBASIC_TOK_COLON),
				AKBASIC_ERR_SYNTAX,
				"FOR EACH takes nothing after the generator call");

	    newenv->isEachLoop = true;
	    newenv->loopFirstLine = firstline;
	    /*
	     * Stashed on forToLeaf rather than cloned into a leaf pool: unlike
	     * DO's condition, this expression is evaluated exactly once, by
	     * akbasic_cmd_for() on this same pass before the per-line leaf
	     * storage it lives in is reused -- see akbasic_runtime_generator_invoke().
	     */
	    newenv->forToLeaf = callexpr;

	    PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
	    PASS(errctx, akbasic_leaf_new_command(expr, "FOR", var));

	    runtime->environment = newenv;
	    *dest = expr;
	    SUCCEED_RETURN(errctx);
	}
    }

    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);
}
