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>
This commit is contained in:
@@ -20,6 +20,86 @@
|
||||
|
||||
#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_));
|
||||
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);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_parse_let(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
Reference in New Issue
Block a user