Consume the COLON token: a line can hold several statements

The token has existed since the scanner was written and nothing read it, so
10 PRINT A$ : REM ... was a parse error. Leading separators are consumed
before each statement and an empty statement yields a NULL leaf rather than
an error, so a trailing colon and a run of them are both legal.

BASIC 7.0 scopes everything after THEN to the condition, which the reference
had no opinion about because it never got here. The rule is not "skip when
false": the rest of the line belongs to whichever arm was written last.

A whole FOR/NEXT on one line still does not loop -- block skipping works by
source line. Recorded in TODO.md as what group A has to fix first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:46:10 -04:00
parent 0f3d8a0ac6
commit a31058cf37
8 changed files with 256 additions and 18 deletions

View File

@@ -52,16 +52,33 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_zero(akbasic_Parser *obj);
/**
* @brief Parse one statement from the token stream.
*
* A line can hold more than one statement, so callers loop on
* akbasic_parser_is_at_end().
* A line can hold more than one statement, separated by colons, so callers loop
* on akbasic_parser_is_at_end(). Leading separators are consumed here.
*
* **`*dest` may be NULL on success.** That is a line that ended in a separator,
* or one that was nothing but separators -- an empty statement, which a C128
* accepts and so does this. A caller that loops must skip a NULL leaf rather
* than handing it to the interpreter.
*
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function.
* @param dest Output destination populated by the function; NULL when the
* remaining tokens held no statement.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not parse.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_parse(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Consume a run of statement separators.
*
* Exposed because a verb with its own parse path may need to know where its
* statement ends. Returns false when nothing but separators remained.
*
* @param obj Parser to advance; a NULL or unbound parser reports false.
* @return `true` when a statement follows the separators.
*/
bool akbasic_parser_skip_separators(akbasic_Parser *obj);
/**
* @brief True when the token stream for this line is spent.
* @param obj Parser to inspect; a NULL or unbound parser is at the end.

View File

@@ -126,6 +126,19 @@ typedef struct akbasic_Runtime
*/
int64_t timems;
/*
* Set by a branch that has decided the remaining statements on its line
* belong to the arm it did not take, and cleared at the top of every line.
*
* This exists because a line can hold several statements and BASIC 7.0
* scopes everything after THEN to the condition -- `IF C THEN A : B` runs
* neither A nor B when C is false. The statement loop is what knows where a
* line ends, so the branch raises a flag and the loop acts on it. See the
* BRANCH case in akbasic_runtime_evaluate() for the exact rule, which is not
* quite "skip when false".
*/
bool skiprestofline;
/* REPL line assembly */
char userline[AKBASIC_MAX_LINE_LENGTH];