Files
akbasic/src/verbs.c
Tachikoma b434be1901 Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.

Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.

verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.

Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.

DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.

Refs libakstdlib#26, libakstdlib#38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00

262 lines
20 KiB
C

/**
* @file verbs.c
* @brief The dispatch table: every verb, function and reserved word in one place.
*/
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h>
#include <akbasic/verbs.h>
#include "verbs.h"
/*
* Sorted by name so bsearch() is valid -- tests/verbs_table.c asserts the
* ordering, because a mis-sorted row silently becomes an unfindable verb rather
* than a compile error.
*
* A NULL parse handler means the rval parses as a plain expression. A NULL exec
* handler means the token is consumed by some other verb's parse handler and is
* never evaluated on its own: ELSE, STEP, THEN and TO are all in that class, as
* are the four reserved words, which exist here only so the scanner can give
* them their token type.
*
* The reference bootstraps MOD, SPC and STR by running a BASIC program of DEF
* statements through the interpreter at startup and keeping their expressions
* (basicruntime_functions.go:14). That hack is not reproduced: they are ordinary
* native handlers here, which removes the need to run the interpreter before the
* interpreter is ready.
*/
static const akbasic_Verb VERBS[] = {
/* name token type arity parse handler exec handler */
{ "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs },
{ "AND", AKBASIC_TOK_AND, -1, NULL, NULL },
{ "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append },
{ "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn },
{ "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto },
{ "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup },
{ "BEGIN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_begin },
{ "BEND", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_bend },
{ "BLOAD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_bload },
{ "BOOT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_boot },
{ "BOX", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_box },
{ "BSAVE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_bsave },
{ "BUMP", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_bump },
{ "CATALOG", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_optional_arglist, akbasic_cmd_directory },
{ "CHAR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_char },
{ "CHR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_chr },
{ "CIRCLE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_circle },
{ "CLR", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_clr },
{ "COLLECT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_collect },
{ "COLLISION", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_collision },
{ "COLOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_color },
{ "CONCAT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_concat },
{ "CONT", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_cont },
{ "COPY", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_copy },
{ "COS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_cos },
{ "DATA", AKBASIC_TOK_COMMAND, -1, akbasic_parse_data, akbasic_cmd_data },
{ "DCLEAR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_dclear },
{ "DCLOSE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_dclose },
{ "DEF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_def, akbasic_cmd_def },
{ "DELETE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_delete },
{ "DIALOG", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_dialog },
{ "DIM", AKBASIC_TOK_COMMAND, -1, akbasic_parse_dim, akbasic_cmd_dim },
{ "DIRECTORY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_optional_arglist, akbasic_cmd_directory },
{ "DLOAD", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dload },
{ "DO", AKBASIC_TOK_COMMAND, -1, akbasic_parse_do, akbasic_cmd_do },
{ "DOPEN", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_dopen },
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "END", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end },
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
{ "GOSUB", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_gosub },
{ "GOTO", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_goto },
{ "GRAPHIC", AKBASIC_TOK_COMMAND, -1, akbasic_parse_graphic, akbasic_cmd_graphic },
{ "GSHAPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_gshape },
{ "HEADER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_header },
{ "HELP", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_help },
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
/*
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
* a `#` as a type suffix and refuses a verb carrying one -- so these rows are
* reached only from akbasic_parse_input() and akbasic_parse_print(), which
* build a leaf with the name directly. They still have to be here, and in
* order, because dispatch is a bsearch on the leaf's name.
*/
{ "INPUT#", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_input_channel },
{ "INSTR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_instr },
{ "KEY", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_key },
{ "LABEL", AKBASIC_TOK_COMMAND, -1, akbasic_parse_label, akbasic_cmd_label },
{ "LEFT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_left },
{ "LEN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_len },
{ "LET", AKBASIC_TOK_COMMAND, -1, akbasic_parse_let, akbasic_cmd_let },
{ "LIST", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_list },
{ "LOAD", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dload },
{ "LOCATE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_locate },
{ "LOG", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_log },
{ "LOOP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_loop, akbasic_cmd_loop },
{ "MENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_menu },
{ "MID", AKBASIC_TOK_FUNCTION, 3, NULL, akbasic_fn_mid },
{ "MOD", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_mod },
{ "MOVSPR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_movspr, akbasic_cmd_movspr },
{ "NEW", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_new },
{ "NEXT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_next },
{ "NOT", AKBASIC_TOK_NOT, -1, NULL, NULL },
{ "ON", AKBASIC_TOK_COMMAND, -1, akbasic_parse_on, akbasic_cmd_on },
{ "OR", AKBASIC_TOK_OR, -1, NULL, NULL },
{ "PAINT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_paint },
{ "PEEK", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_peek },
{ "PLAY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_play },
{ "POINT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_point, akbasic_cmd_point },
{ "POINTER", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_pointer },
{ "POINTERVAR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_pointervar },
{ "POKE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_poke, akbasic_cmd_poke },
{ "PRINT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_print, akbasic_cmd_print },
{ "PRINT#", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_print_channel },
{ "PUDEF", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_pudef },
{ "QUIT", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_quit },
{ "RAD", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rad },
{ "RCOLLISION", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rcollision },
{ "READ", AKBASIC_TOK_COMMAND, -1, akbasic_parse_read, akbasic_cmd_read },
{ "RECORD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_record },
{ "REM", AKBASIC_TOK_REM, -1, NULL, NULL },
{ "RENAME", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_rename },
{ "RENUMBER", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_optional_arglist, akbasic_cmd_renumber },
{ "RESTORE", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_restore },
{ "RESUME", AKBASIC_TOK_COMMAND, -1, akbasic_parse_resume, akbasic_cmd_resume },
{ "RETURN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_return },
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
{ "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu },
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
{ "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit },
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },
{ "RSPRITE", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsprite },
{ "RUN", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_run },
{ "RWINDOW", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rwindow },
{ "SAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
{ "SCALE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_scale },
{ "SCNCLR", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_scnclr },
{ "SCRATCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_scratch },
{ "SGN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_sgn },
{ "SHL", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_shl },
{ "SHR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_shr },
{ "SIN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_sin },
{ "SLEEP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sleep },
{ "SOLID", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_solid },
{ "SOUND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sound },
{ "SPC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_spc },
{ "SPRCOLOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sprcolor },
{ "SPRHIT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sprhit },
{ "SPRITE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sprite },
{ "SPRSAV", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sprsav },
{ "SSHAPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sshape },
{ "STASH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_stash },
{ "STEP", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "STOP", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_stop },
{ "STR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_str },
{ "SWAP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_swap },
{ "SYS", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sys },
{ "TAN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_tan },
{ "TEMPO", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_tempo },
{ "THEN", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "TO", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "TRAP", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_trap },
{ "TROFF", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_troff },
{ "TRON", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_tron },
{ "TYPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_type, akbasic_cmd_type },
{ "UISTYLE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_uistyle },
{ "UNTIL", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "USING", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "VAL", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_val },
{ "VERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
{ "VOL", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_vol },
{ "WAIT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_wait },
{ "WHILE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
{ "WIDTH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_width },
{ "WINDOW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_window },
{ "XOR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_xor },
};
#define VERB_COUNT ((int)(sizeof(VERBS) / sizeof(VERBS[0])))
/**
* @brief bsearch(3) comparator for the verb table.
*
* **This is the one call in `src/` that stays on raw `strcmp` on purpose.**
* Everything else that reaches past libakstdlib to a function that library wraps
* has been converted to the wrapper and propagates the error context; the ruling
* that required it is libakstdlib #38, and the rule is that a function which
* cannot report an error changes its own signature rather than swallowing one.
*
* That remedy is unavailable here, and only here. `bsearch(3)` fixes the
* comparator's signature at `int (*)(const void *, const void *)`, so there is
* no out-parameter to hand an `akerr_ErrorContext *` back through and no return
* value that is not already the ordering. Converting this one site means
* dropping `bsearch` for an in-house binary search that can propagate -- a
* separate change to a lookup that runs once per scanned identifier, and a
* decision that belongs in its own issue rather than riding along with a port.
*
* The bypass is safe as written: `key` is the caller's NUL-terminated `upper`
* buffer and `element->name` is a string literal in the static table above, so
* neither argument can be NULL and neither can be unterminated -- which is the
* whole of what `aksl_strcmp` would have checked.
*/
static int verb_compare(const void *key, const void *element)
{
const akbasic_Verb *verb = (const akbasic_Verb *)element;
return strcmp((const char *)key, verb->name);
}
const akbasic_Verb *akbasic_verb_table(int *count)
{
if ( count != NULL ) {
*count = VERB_COUNT;
}
return VERBS;
}
akerr_ErrorContext *akbasic_verb_lookup(const char *name, const akbasic_Verb **dest)
{
PREPARE_ERROR(errctx);
char upper[AKBASIC_MAX_STRING_LENGTH];
size_t i = 0;
size_t len = 0;
FAIL_ZERO_RETURN(errctx, (name != NULL), AKERR_NULLPOINTER, "NULL name in verb lookup");
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in verb lookup");
*dest = NULL;
PASS(errctx, aksl_strlen(name, &len));
if ( len == 0 || len >= sizeof(upper) ) {
/* Too long to be any verb we know. Not an error, just a miss. */
SUCCEED_RETURN(errctx);
}
for ( i = 0; i < len; i++ ) {
upper[i] = (char)toupper((unsigned char)name[i]);
}
upper[len] = '\0';
*dest = (const akbasic_Verb *)bsearch(upper, VERBS, VERB_COUNT, sizeof(VERBS[0]), verb_compare);
SUCCEED_RETURN(errctx);
}