Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
|
|
|
/**
|
|
|
|
|
* @file parser_commands.c
|
|
|
|
|
* @brief Verbs that need their own parse path rather than a plain expression.
|
|
|
|
|
*
|
|
|
|
|
* Ported from basicparser_commands.go. Two of these mutate runtime state from
|
|
|
|
|
* inside the parser and that is not an accident: DEF installs the function so a
|
|
|
|
|
* later line can call it, and FOR builds and installs the loop's environment
|
|
|
|
|
* before the body is ever scanned. The waitingForCommand scheme depends on the
|
|
|
|
|
* latter.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/parser.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
|
|
|
|
|
#include "verbs.h"
|
|
|
|
|
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:17:48 -04:00
|
|
|
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_));
|
|
|
|
|
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->right != NULL ) {
|
|
|
|
|
tail = tail->right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"DRAW expected X after TO");
|
|
|
|
|
tail = tail->right;
|
|
|
|
|
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->right));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (tail->right != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"DRAW expected Y after TO X,");
|
|
|
|
|
tail = tail->right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
|
|
|
|
PASS(errctx, akbasic_leaf_new_command(expr, operator_->lexeme, arglist));
|
|
|
|
|
*dest = expr;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 23:53:56 -04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest));
|
|
|
|
|
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->right ) {
|
|
|
|
|
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 .right chain, which for READ is the
|
|
|
|
|
* *next* identifier, not a subscript. Sever it so evaluating this leaf
|
|
|
|
|
* cannot walk into its sibling.
|
|
|
|
|
*/
|
|
|
|
|
env->readIdentifierLeaves[i]->right = NULL;
|
|
|
|
|
expr = expr->right;
|
|
|
|
|
}
|
|
|
|
|
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->right ) {
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_parser_relation(parser, &relation));
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|