/** * @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 #include #include #include #include #include #include #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); } akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *tail = NULL; akbasic_Token *peeked = NULL; akbasic_Token *operator_ = NULL; /* * DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs * instead of a comma. TO is a reserved word the argument list will not cross, * so the pairs after the first are appended by hand and the whole thing * flattens to one argument chain: source, x1, y1, x2, y2, ... The exec * handler walks it in pairs and never has to know a TO was involved. */ PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist)); FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX, "DRAW expected a color source and a coordinate"); tail = arglist->right; while ( tail->next != NULL ) { tail = tail->next; } for ( ;; ) { peeked = akbasic_parser_peek(parser); if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND || strcmp(peeked->lexeme, "TO") != 0 ) { break; } FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "DRAW expected TO"); PASS(errctx, akbasic_parser_expression(parser, &tail->next)); FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX, "DRAW expected X after TO"); tail = tail->next; FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA), AKBASIC_ERR_SYNTAX, "DRAW expected TO X,Y"); PASS(errctx, akbasic_parser_expression(parser, &tail->next)); FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX, "DRAW expected Y after TO X,"); tail = tail->next; } PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist)); *dest = expr; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_parse_movspr(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *form = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *tail = NULL; akbasic_Token *operator_ = NULL; akbasic_Token *peeked = NULL; const char *formlexeme = "0"; /* * MOVSPR is the one verb in the language whose arguments are not simply * comma-separated. It has four forms and the separators are what tell them * apart: * * MOVSPR n, x, y absolute * MOVSPR n, +x, -y relative -- a signed coordinate, not a negative one * MOVSPR n, d ; a polar: distance, then angle * MOVSPR n, a # s continuous: angle, then speed * * So the form has to be decided here, where the tokens still exist, and * carried to the exec handler somehow. It is carried as a synthetic integer * literal prepended to the argument list, which flattens the whole statement * to `form, n, a, b` -- the same trick akbasic_parse_draw uses to make a * polyline look like a plain argument chain. */ PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; PASS(errctx, akbasic_parser_new_leaf(parser, &form)); PASS(errctx, akbasic_parser_expression(parser, &arglist->right)); FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX, "MOVSPR expected a sprite number"); tail = arglist->right; FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA), AKBASIC_ERR_SYNTAX, "MOVSPR expected a comma after the sprite number"); /* * A leading sign makes it relative. `+` has to be consumed here because the * expression parser has no unary plus; `-` is left where it is, because * unary minus is a real operator and re-parsing it would drop the sign. */ peeked = akbasic_parser_peek(parser); if ( peeked != NULL && (peeked->tokentype == AKBASIC_TOK_PLUS || peeked->tokentype == AKBASIC_TOK_MINUS) ) { formlexeme = "1"; if ( peeked->tokentype == AKBASIC_TOK_PLUS ) { (void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS); } } PASS(errctx, akbasic_parser_expression(parser, &tail->next)); FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX, "MOVSPR expected a coordinate, a distance or an angle"); tail = tail->next; if ( akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON) ) { formlexeme = "2"; } else if ( akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK) ) { formlexeme = "3"; } else { FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA), AKBASIC_ERR_SYNTAX, "MOVSPR expected `, y', `; angle' or `# speed'"); if ( formlexeme[0] == '0' ) { peeked = akbasic_parser_peek(parser); if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_PLUS ) { formlexeme = "1"; (void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS); } else if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_MINUS ) { formlexeme = "1"; } } else if ( akbasic_parser_peek(parser) != NULL && akbasic_parser_peek(parser)->tokentype == AKBASIC_TOK_PLUS ) { (void)akbasic_parser_match1(parser, AKBASIC_TOK_PLUS); } } PASS(errctx, akbasic_parser_expression(parser, &tail->next)); FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX, "MOVSPR expected a second value"); PASS(errctx, akbasic_leaf_new_literal_int(form, formlexeme)); form->next = arglist->right; arglist->right = form; PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist)); *dest = expr; SUCCEED_RETURN(errctx); } /** * @brief Read an optional `WHILE`/`UNTIL` condition off a DO or a LOOP. * * Both ends of a DO/LOOP take the same optional clause, so both parse it the * same way. @p kind comes back as one of the AKBASIC_LOOPCOND_* values and * @p condition is NULL when there was no clause. */ static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_ASTLeaf **condition, int *kind) { PREPARE_ERROR(errctx); akbasic_Token *peeked = NULL; *condition = NULL; *kind = AKBASIC_LOOPCOND_NONE; peeked = akbasic_parser_peek(parser); if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) { SUCCEED_RETURN(errctx); } if ( strcmp(peeked->lexeme, "WHILE") == 0 ) { *kind = AKBASIC_LOOPCOND_WHILE; } else if ( strcmp(peeked->lexeme, "UNTIL") == 0 ) { *kind = AKBASIC_LOOPCOND_UNTIL; } else { SUCCEED_RETURN(errctx); } (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); /* A loop condition is a condition, so a lone `=` in it is an equality test. */ parser->comparing = true; PASS(errctx, akbasic_parser_expression(parser, condition)); parser->comparing = false; FAIL_ZERO_RETURN(errctx, (*condition != NULL), AKBASIC_ERR_SYNTAX, "Expected a condition after WHILE or UNTIL"); SUCCEED_RETURN(errctx); } /* * DO [WHILE|UNTIL (condition)] * * Pushes the loop's scope at parse time, exactly as FOR does and for the same * reason: the condition leaf has to outlive the line it was written on, and an * environment's leaf pool is what gives it somewhere to live. */ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_Runtime *runtime = parser->runtime; akbasic_Environment *parent = runtime->environment; akbasic_Environment *newenv = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *condition = NULL; int kind = AKBASIC_LOOPCOND_NONE; int64_t firstline = parent->lineno + 1; PASS(errctx, akbasic_runtime_new_environment(runtime)); newenv = runtime->environment; runtime->environment = parent; /* * Parsed against the parent's token stream but stored in the new scope -- * the body is scanned line by line afterwards, and the new scope must not be * active until parsing is done. */ PASS(errctx, parse_loop_condition(parser, &condition, &kind)); newenv->isDoLoop = true; newenv->loopFirstLine = firstline; newenv->doConditionKind = kind; if ( condition != NULL ) { PASS(errctx, akbasic_leaf_clone(condition, &newenv->doLeafPool, &newenv->doConditionLeaf)); } PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "DO", NULL)); runtime->environment = newenv; *dest = expr; SUCCEED_RETURN(errctx); } /* * LOOP [WHILE|UNTIL (condition)] * * The condition hangs off the command leaf's `.right`, with the WHILE/UNTIL * choice in that leaf's integer literal and the expression on its `.left`. No * cloning: this line is still scanned when the verb runs. */ akerr_ErrorContext *akbasic_parse_loop(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *holder = NULL; akbasic_ASTLeaf *condition = NULL; int kind = AKBASIC_LOOPCOND_NONE; PASS(errctx, parse_loop_condition(parser, &condition, &kind)); PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); if ( condition != NULL ) { PASS(errctx, akbasic_parser_new_leaf(parser, &holder)); PASS(errctx, akbasic_leaf_init(holder, AKBASIC_LEAF_LITERAL_INT)); holder->literal_int = kind; holder->left = condition; } PASS(errctx, akbasic_leaf_new_command(expr, "LOOP", holder)); *dest = expr; SUCCEED_RETURN(errctx); } /* * ON (expression) GOTO|GOSUB (target) [, ...] * * Flattened to one argument chain: a marker carrying the GOSUB flag, the * selector expression, then the targets. Same trick akbasic_parse_draw and * akbasic_parse_movspr use, so the exec handler walks a plain list. */ akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *marker = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *tail = NULL; akbasic_Token *operator_ = NULL; bool gosub = false; PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; PASS(errctx, akbasic_parser_new_leaf(parser, &marker)); PASS(errctx, akbasic_leaf_init(marker, AKBASIC_LEAF_LITERAL_INT)); PASS(errctx, akbasic_parser_expression(parser, &marker->next)); FAIL_ZERO_RETURN(errctx, (marker->next != NULL), AKBASIC_ERR_SYNTAX, "Expected ON (expression) GOTO|GOSUB (line) [, ...]"); tail = marker->next; FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); if ( strcmp(operator_->lexeme, "GOSUB") == 0 ) { gosub = true; } else { FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "GOTO"), AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)"); } marker->literal_int = (gosub ? 1 : 0); for ( ;; ) { PASS(errctx, akbasic_parser_expression(parser, &tail->next)); FAIL_ZERO_RETURN(errctx, (tail->next != NULL), AKBASIC_ERR_SYNTAX, "Expected a line number or label after ON ... %s", (gosub ? "GOSUB" : "GOTO")); tail = tail->next; if ( !akbasic_parser_match1(parser, AKBASIC_TOK_COMMA) ) { break; } } arglist->right = marker; PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "ON", arglist)); *dest = expr; SUCCEED_RETURN(errctx); } /* * RESUME [NEXT | (line)] * * `NEXT` is a COMMAND token, so the default command path -- which parses the * rval as an expression -- cannot read it: `RESUME NEXT` came back as "Expected * expression or literal". The word is kept as the command leaf's rval so the * exec handler can tell the three forms apart by looking at it. */ akerr_ErrorContext *akbasic_parse_resume(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *target = NULL; akbasic_Token *peeked = NULL; peeked = akbasic_parser_peek(parser); if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND && strcmp(peeked->lexeme, "NEXT") == 0 ) { (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); PASS(errctx, akbasic_parser_new_leaf(parser, &target)); PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL)); } else if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_parser_expression(parser, &target)); } PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "RESUME", target)); *dest = expr; SUCCEED_RETURN(errctx); } /* * PRINT [USING (format);] (expression) * * Only the USING form needs a parse handler; without it PRINT keeps the default * path, which parses one expression. `USING` is a COMMAND token, so the default * path could not read it -- the same reason RESUME needed one for `NEXT`. * * The result is a command leaf whose rval is an argument list of exactly two: * the format and the value. A plain PRINT keeps its single-expression rval, so * the exec handler tells them apart by looking for the list. */ akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *right = NULL; akbasic_Token *peeked = NULL; peeked = akbasic_parser_peek(parser); /* * `PRINT #1, expr` writes to a file channel. A C128 spells it `PRINT#1` with * no space, which this scanner cannot produce: `PRINT#` reads as an * identifier with an integer suffix, and a verb name carrying a suffix is * refused (deviation 30). So the `#` is its own token and the space before * it is required. Recorded in TODO.md section 5. * * The channel form is marked by a leaf named "PRINT#" holding an argument * list of two: the channel and the value. */ if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) { akbasic_ASTLeaf *arglist = NULL; (void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; PASS(errctx, akbasic_parser_expression(parser, &arglist->right)); FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX, "Expected PRINT #(channel), (expression)"); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA), AKBASIC_ERR_SYNTAX, "Expected a comma after PRINT #(channel)"); PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next)); FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX, "Expected PRINT #(channel), (expression)"); PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "PRINT#", arglist)); *dest = expr; SUCCEED_RETURN(errctx); } if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND || strcmp(peeked->lexeme, "USING") != 0 ) { /* The ordinary PRINT: one expression, or none at all. */ if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_parser_expression(parser, &right)); } PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", right)); *dest = expr; SUCCEED_RETURN(errctx); } (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; PASS(errctx, akbasic_parser_expression(parser, &arglist->right)); FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX, "Expected PRINT USING (format); (expression)"); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_SEMICOLON), AKBASIC_ERR_SYNTAX, "Expected a semicolon between PRINT USING's format and its value"); PASS(errctx, akbasic_parser_expression(parser, &arglist->right->next)); FAIL_ZERO_RETURN(errctx, (arglist->right->next != NULL), AKBASIC_ERR_SYNTAX, "Expected PRINT USING (format); (expression)"); PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "PRINT", arglist)); *dest = expr; SUCCEED_RETURN(errctx); } /* * The argument list, for verbs that may be written with no arguments at all -- * bare `KEY` lists the macros, bare `RENUMBER` renumbers with its defaults. * `akbasic_parse_arglist` insists on at least one argument, which is right for * every other verb that uses it. */ akerr_ErrorContext *akbasic_parse_optional_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *expr = NULL; akbasic_Token *operator_ = NULL; akbasic_Token *peeked = NULL; peeked = akbasic_parser_peek(parser); if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_parse_arglist(parser, dest)); SUCCEED_RETURN(errctx); } PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, NULL)); *dest = expr; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_parse_let(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); /* * LET is optional in this dialect and in Commodore BASIC 7.0. Assignment is * handled by expression evaluation, so LET parses as a bare assignment and * its exec handler does nothing. */ PASS(errctx, akbasic_parser_assignment(parser, dest)); SUCCEED_RETURN(errctx); } /* LABEL and DIM share a shape: the verb, then one identifier. */ static akerr_ErrorContext *parse_verb_with_identifier(akbasic_Parser *parser, const char *verbname, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *identifier = NULL; akbasic_ASTLeaf *command = NULL; PASS(errctx, akbasic_parser_primary(parser, &identifier)); FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX, "Expected identifier"); PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, verbname, identifier)); *dest = command; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_parse_label(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); PASS(errctx, parse_verb_with_identifier(parser, "LABEL", dest)); SUCCEED_RETURN(errctx); } /** * @brief `DIM NAME(SIZES)`, or `DIM NAME@ AS TYPE` / `DIM NAME@ AS PTR TO TYPE`. * * The `AS` clause is read here and parked on the *command* leaf: `literal_string` * takes the type name and `literal_int` records whether `PTR TO` was written. * Both fields are free on a command leaf, which is the same reasoning the field * leaf uses for `.left` -- this file's header note about three defects from one * overloaded link field is about giving a field two meanings on the *same* leaf * type, not about using an unused one. * * `AS`, `PTR` and `TO` are matched by lexeme rather than promoted to keywords. * Making them reserved words would break any existing program with a variable * called `AS#`, and they are only meaningful in this one position. */ /** * @brief `POINT P@ AT target`. * * Two expressions with a word between them rather than a comma, because that is * how it reads: the verb says what is being done and `AT` says which way round * the two operands go. `POINT P@, A@` would read equally well in either * direction, and getting it backwards would silently point the wrong thing. * * The two are chained through `.next`, the argument link, so the runtime reaches * them with akbasic_leaf_first_argument() like any other verb's operands. */ akerr_ErrorContext *akbasic_parse_point(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *pointer = NULL; akbasic_ASTLeaf *target = NULL; akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *command = NULL; akbasic_Token *word = NULL; PASS(errctx, akbasic_parser_expression(parser, &pointer)); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected POINT AT "); PASS(errctx, akbasic_parser_previous(parser, &word)); FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AT") == 0), AKBASIC_ERR_SYNTAX, "Expected AT after POINT , not %s", word->lexeme); PASS(errctx, akbasic_parser_expression(parser, &target)); pointer->next = target; PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; arglist->right = pointer; PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "POINT", arglist)); *dest = command; SUCCEED_RETURN(errctx); } /** * @brief `TYPE NAME`, whose body was already read by the prescan. * * Only the opening line is ever parsed: the verb jumps past its own `END TYPE`, * so the field lines never reach the parser at all. That is deliberate -- a * field line is a declaration, and `W#` on its own would otherwise evaluate as a * bare identifier and quietly create a global called `W#`. */ akerr_ErrorContext *akbasic_parse_type(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); PASS(errctx, parse_verb_with_identifier(parser, "TYPE", dest)); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *command = NULL; akbasic_Token *word = NULL; PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest)); command = *dest; if ( !akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER) ) { SUCCEED_RETURN(errctx); } PASS(errctx, akbasic_parser_previous(parser, &word)); FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX, "Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after AS"); PASS(errctx, akbasic_parser_previous(parser, &word)); if ( strcasecmp(word->lexeme, "PTR") == 0 ) { command->literal_int = 1; /* * `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a * command token rather than an identifier. Matched by lexeme here for * the same reason AS and PTR are: these three words mean something only * in this one position, and reserving them would break any program with * a variable called TO#. */ FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected TO after PTR"); PASS(errctx, akbasic_parser_previous(parser, &word)); FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX, "Expected PTR TO TYPENAME, not PTR %s", word->lexeme); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO"); PASS(errctx, akbasic_parser_previous(parser, &word)); } snprintf(command->literal_string, sizeof(command->literal_string), "%s", word->lexeme); SUCCEED_RETURN(errctx); } /* * DEF NAME (A, ...) [= expression] * COMMAND IDENTIFIER ARGUMENTLIST [ASSIGNMENT EXPRESSION] * * With an `=` the function is a single expression. Without one it is a * multi-line subroutine whose body starts on the next line and ends at RETURN, * so the environment is told to skip forward to that RETURN rather than execute * the body during the definition. */ 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 *walk = NULL; akbasic_ASTLeaf *command = NULL; akbasic_FunctionDef *fndef = NULL; size_t i = 0; PASS(errctx, akbasic_parser_primary(parser, &identifier)); FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER), AKBASIC_ERR_SYNTAX, "Expected identifier"); PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, true, &arglist)); FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX, "Expected argument list (identifier names)"); for ( walk = arglist->right; walk != NULL; walk = walk->next ) { FAIL_ZERO_RETURN(errctx, (walk->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING || walk->leaftype == AKBASIC_LEAF_IDENTIFIER_INT || walk->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT), AKBASIC_ERR_SYNTAX, "Only variable identifiers are valid arguments for DEF"); } PASS(errctx, akbasic_runtime_new_function(runtime, &fndef)); /* Uppercase the name: verbs and function names are case-insensitive. */ FAIL_ZERO_RETURN(errctx, (strlen(identifier->identifier) < sizeof(fndef->name)), AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier); for ( i = 0; i < strlen(identifier->identifier); i++ ) { char c = identifier->identifier[i]; fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c); } fndef->name[strlen(identifier->identifier)] = '\0'; if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) { PASS(errctx, akbasic_parser_expression(parser, &expression)); PASS(errctx, akbasic_leaf_clone(expression, &fndef->leafpool, &fndef->expression)); } else { /* * No expression: the body is the lines that follow. Record where it * starts and skip to RETURN so the definition itself does not execute * the body. */ fndef->expression = NULL; PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "RETURN")); } PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist)); fndef->lineno = runtime->environment->lineno + 1; PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0)); PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "DEF", NULL)); *dest = command; SUCCEED_RETURN(errctx); } /* * FOR ... TO .... [STEP ...] * COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION] * * Sets up the loop's environment with the TO and STEP expressions and the first * body line, then makes it the active environment. The FOR leaf itself carries * the assignment. */ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_Runtime *runtime = parser->runtime; akbasic_ASTLeaf *assignment = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_Token *operator_ = NULL; akbasic_Environment *parent = runtime->environment; akbasic_Environment *newenv = NULL; int64_t firstline = 0; PASS(errctx, akbasic_parser_assignment(parser, &assignment)); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "TO"), AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); FAIL_ZERO_RETURN(errctx, (assignment != NULL && akbasic_leaf_is_identifier(assignment->left)), AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); firstline = parent->lineno + 1; /* * The loop body is scanned against the *parent* environment's token stream, * so the new environment cannot become active until parsing is done. Parse * TO and STEP first, then switch. */ PASS(errctx, akbasic_runtime_new_environment(runtime)); newenv = runtime->environment; runtime->environment = parent; PASS(errctx, akbasic_parser_expression(parser, &newenv->forToLeaf)); if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { PASS(errctx, akbasic_parser_previous(parser, &operator_)); FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "STEP"), AKBASIC_ERR_SYNTAX, "Expected FOR (assignment) TO (expression) [STEP (expression)]"); PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf)); } else { /* * Dartmouth BASIC says not to infer a negative step: it is either given * explicitly or assumed to be +1. */ PASS(errctx, akbasic_parser_new_leaf(parser, &newenv->forStepLeaf)); PASS(errctx, akbasic_leaf_new_literal_int(newenv->forStepLeaf, "1")); } /* A NEXT already being awaited means this is an inner loop over the same variable. */ if ( strcmp(parent->waitingForCommand, "NEXT") == 0 ) { newenv->forNextVariable = parent->forNextVariable; } newenv->loopFirstLine = firstline; PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "FOR", assignment)); runtime->environment = newenv; *dest = expr; SUCCEED_RETURN(errctx); } /* * READ VARNAME [, ...] * COMMAND ARGUMENTLIST * * The identifier leaves are deep-copied into the environment, because the DATA * line that fills them is parsed later and will have overwritten the per-line * leaf pool by then. */ akerr_ErrorContext *akbasic_parse_read(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_Environment *env = parser->runtime->environment; akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *command = NULL; int i = 0; PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist)); FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX, "Expected identifier"); env->readLeafPool.next = 0; expr = arglist->right; for ( i = 0; i < AKBASIC_MAX_LEAVES; i++ ) { if ( expr == NULL ) { env->readIdentifierLeaves[i] = NULL; continue; } FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr), AKBASIC_ERR_SYNTAX, "Expected identifier"); PASS(errctx, akbasic_leaf_clone(expr, &env->readLeafPool, &env->readIdentifierLeaves[i])); /* * A cloned identifier keeps its sibling chain, which for READ is the * *next* identifier. Sever it so evaluating this leaf cannot walk into * its sibling. */ env->readIdentifierLeaves[i]->next = NULL; expr = expr->next; } env->readReturnLine = env->lineno + 1; PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "READ", arglist)); *dest = command; SUCCEED_RETURN(errctx); } /* * DATA LITERAL [, ...] * COMMAND ARGUMENTLIST */ akerr_ErrorContext *akbasic_parse_data(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *command = NULL; PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist)); FAIL_ZERO_RETURN(errctx, (arglist != NULL && arglist->right != NULL), AKBASIC_ERR_SYNTAX, "Expected literal"); for ( expr = arglist->right; expr != NULL; expr = expr->next ) { FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_literal(expr), AKBASIC_ERR_SYNTAX, "Expected literal"); } PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "DATA", arglist)); *dest = command; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_parse_poke(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *expr = NULL; PASS(errctx, akbasic_parser_argument_list(parser, AKBASIC_TOK_FUNCTION_ARGUMENT, false, &arglist)); FAIL_ZERO_RETURN(errctx, (arglist != NULL), AKBASIC_ERR_SYNTAX, "POKE expected INTEGER, INTEGER"); PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_command(expr, "POKE", arglist)); *dest = expr; SUCCEED_RETURN(errctx); } /* * IF relation THEN command [ELSE command] * becomes BRANCH(expr=relation, left=then_command, right=else_command). */ akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *relation = NULL; akbasic_ASTLeaf *then_command = NULL; akbasic_ASTLeaf *else_command = NULL; akbasic_ASTLeaf *branch = NULL; akbasic_Token *operator_ = NULL; /* * Everything from here to the THEN is a condition, so a lone `=` in it is an * equality test. Cleared immediately afterwards: the statement the THEN arm * holds may well be an assignment. */ /* * The whole expression, not just a relation. The reference parses one * relation here, which is below AND and OR in the grammar chain -- so * `IF A# = 5 AND B# = 3 THEN` stopped after the first comparison, left the * AND unconsumed, and reported "Incomplete IF statement" against a line that * is ordinary BASIC. A condition is an expression. */ parser->comparing = true; PASS(errctx, akbasic_parser_expression(parser, &relation)); parser->comparing = false; FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, "Incomplete IF statement"); PASS(errctx, akbasic_parser_previous(parser, &operator_)); FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "THEN"), AKBASIC_ERR_SYNTAX, "Expected IF ... THEN"); PASS(errctx, akbasic_parser_command(parser, &then_command)); if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { PASS(errctx, akbasic_parser_previous(parser, &operator_)); FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "ELSE"), AKBASIC_ERR_SYNTAX, "Expected IF ... THEN ... ELSE ..."); PASS(errctx, akbasic_parser_command(parser, &else_command)); } PASS(errctx, akbasic_parser_new_leaf(parser, &branch)); PASS(errctx, akbasic_leaf_new_branch(branch, relation, then_command, else_command)); *dest = branch; SUCCEED_RETURN(errctx); } /* * INPUT "PROMPT" VARIABLE * COMMAND EXPRESSION IDENTIFIER * * The prompt is hung off the identifier's .left, which is where the exec handler * looks for it. */ akerr_ErrorContext *akbasic_parse_input(akbasic_Parser *parser, akbasic_ASTLeaf **dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *promptexpr = NULL; akbasic_ASTLeaf *identifier = NULL; akbasic_ASTLeaf *command = NULL; akbasic_Token *peeked = NULL; /* * `INPUT #1, VAR` reads a line from a file channel rather than from the * sink. Same spelling note as PRINT: the `#` is its own token and the space * before it is required. */ peeked = akbasic_parser_peek(parser); if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_HASHMARK ) { akbasic_ASTLeaf *arglist = NULL; (void)akbasic_parser_match1(parser, AKBASIC_TOK_HASHMARK); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; PASS(errctx, akbasic_parser_expression(parser, &arglist->right)); FAIL_ZERO_RETURN(errctx, (arglist->right != NULL), AKBASIC_ERR_SYNTAX, "Expected INPUT #(channel), (variable)"); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMA), AKBASIC_ERR_SYNTAX, "Expected a comma after INPUT #(channel)"); PASS(errctx, akbasic_parser_primary(parser, &arglist->right->next)); FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(arglist->right->next), AKBASIC_ERR_SYNTAX, "Expected INPUT #(channel), (variable)"); PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "INPUT#", arglist)); *dest = command; SUCCEED_RETURN(errctx); } PASS(errctx, akbasic_parser_expression(parser, &promptexpr)); PASS(errctx, akbasic_parser_primary(parser, &identifier)); FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX, "Expected identifier"); PASS(errctx, akbasic_parser_new_leaf(parser, &command)); PASS(errctx, akbasic_leaf_new_command(command, "INPUT", identifier)); identifier->left = promptexpr; *dest = command; SUCCEED_RETURN(errctx); }