Read the keyboard: implement GET, GETKEY and SCNCLR

The poll_key half of group E, against the akbasic_InputBackend record. The
libakgl implementation behind it is akgl_controller_poll_key(), which drains a
ring the library fills from SDL events the host pumps -- so the interpreter can
answer "is there a key waiting" without owning an event loop, which is what goal
3 requires.

GET and GETKEY differ in one way and it is the interesting one. GET takes
whatever is there including nothing, and an empty buffer is success with the
empty string rather than an error -- that is what happens on most iterations of
every GET loop ever written, and upstream is explicit that its poll reports it
the same way. GETKEY waits, and since the library may not block, waiting is
spelled as holding the step loop: the verb sets a flag and akbasic_runtime_step()
declines to advance until a key arrives. Every step still returns and a bounded
run() still comes back, so a host keeps its frame rate; the program simply does
not move past the GETKEY.

The PLAY queue is serviced before that check on purpose -- music should keep
playing while a program waits for a keypress. Withdrawing the input device while
a GETKEY is holding releases it rather than wedging the script on a device that
no longer exists.

Both verbs accept an integer variable as well as a string one and give it the raw
key code, which is what a program testing for cursor or function keys needs. No
key is code zero, matching what a C128 reports. A float variable is refused: it
is neither a character nor a code.

SCNCLR goes through the text sink rather than a device, because the sink is where
PRINT already goes and is the only thing that knows what a screen means for this
host. The stdio sink treats it as a no-op; clearing a pipe means nothing.

One thing worth knowing before writing any bounded-run test, and now commented in
tests/input_verbs.c: source lines are stored indexed by line number and the
cursor starts at zero, so a step is spent on each empty slot along the way. A
program at line 10 needs eleven steps before it has run anything.

70/70 ctest, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:35:05 -04:00
parent e24926d429
commit 83185bafa8
11 changed files with 508 additions and 4 deletions

View File

@@ -824,6 +824,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc
akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
bool blocked = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in step");
@@ -839,6 +840,18 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
SUCCEED_RETURN(errctx);
}
/*
* A GETKEY with nothing typed yet holds the program here. The step still
* returns -- a host keeps its frame rate and a bounded run() still comes
* back -- it simply does not advance, which is what GETKEY means. The note
* queue above is serviced first on purpose: music should keep playing while
* a program waits for a keypress.
*/
PASS(errctx, akbasic_input_service(obj, &blocked));
if ( blocked ) {
SUCCEED_RETURN(errctx);
}
PASS(errctx, akbasic_runtime_zero(obj));
PASS(errctx, akbasic_scanner_zero(obj));

217
src/runtime_input.c Normal file
View File

@@ -0,0 +1,217 @@
/**
* @file runtime_input.c
* @brief The console input verbs: GET, GETKEY and SCNCLR.
*
* Same rule as the other two device files -- no SDL here, everything through the
* akbasic_InputBackend record. The libakgl implementation behind it is
* akgl_controller_poll_key(), which drains a ring the library fills from SDL
* events the *host* pumps, so the interpreter answers "is there a key waiting"
* without owning an event loop.
*
* GET and GETKEY differ in exactly one way and it is the interesting one. GET
* takes whatever is there, including nothing. GETKEY waits -- and since the
* library may not block, waiting is spelled as holding the step loop rather than
* as sitting on the keyboard. See TODO.md section 5.
*/
#include <string.h>
#include <akerror.h>
#include <akbasic/error.h>
#include <akbasic/input.h>
#include <akbasic/runtime.h>
#include "verbs.h"
/* Most verbs answer "did something happen"; this is that answer. */
#define SUCCEED_TRUE(__obj, __dest) \
do { \
*(__dest) = &(__obj)->staticTrueValue; \
} while ( 0 )
/** @brief Refuse politely when the host lent us no input device. */
static akerr_ErrorContext *require_input(akbasic_Runtime *obj, const char *verb)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
"NULL argument in require_input");
FAIL_ZERO_RETURN(errctx, (obj->input != NULL), AKBASIC_ERR_DEVICE,
"%s needs an input device and this runtime has none", verb);
SUCCEED_RETURN(errctx);
}
/**
* @brief Identify the variable a GET or GETKEY assigns into.
*
* A string variable receives the character; a numeric one receives the key code,
* which is what a program testing for cursor keys or function keys wants.
*/
static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, akbasic_Variable **dest, bool *numeric)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *arg = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL && numeric != NULL),
AKERR_NULLPOINTER, "NULL argument in target_variable");
arg = akbasic_leaf_first_argument(expr);
if ( arg == NULL ) {
arg = expr->right;
}
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
"%s expected a variable", verb);
FAIL_ZERO_RETURN(errctx,
(arg->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT),
AKBASIC_ERR_TYPE,
"%s expected a string or integer variable", verb);
*numeric = (arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT);
PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest));
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED,
"%s could not reach the variable %s", verb, arg->identifier);
SUCCEED_RETURN(errctx);
}
/** @brief Write a keystroke, or the absence of one, into the target variable. */
static akerr_ErrorContext *store_key(akbasic_Variable *variable, bool numeric, int keycode, bool available)
{
PREPARE_ERROR(errctx);
int64_t zerosubscript[1] = { 0 };
char text[2];
if ( numeric ) {
/* No key is code zero, which is what a C128 reports too. */
PASS(errctx, akbasic_variable_set_integer(variable, available ? (int64_t)keycode : 0,
zerosubscript, 1));
SUCCEED_RETURN(errctx);
}
text[0] = (available && keycode > 0 && keycode < 128) ? (char)keycode : '\0';
text[1] = '\0';
PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- GET --- */
akerr_ErrorContext *akbasic_cmd_get(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
bool numeric = false;
bool available = false;
int keycode = 0;
(void)lval; (void)rval;
PASS(errctx, require_input(obj, "GET"));
PASS(errctx, target_variable(obj, expr, "GET", &variable, &numeric));
/*
* An empty buffer is success with the empty string, not an error. That is
* what happens on most iterations of any GET loop ever written, and libakgl
* is explicit that its poll reports it the same way.
*/
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
PASS(errctx, store_key(variable, numeric, keycode, available));
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- GETKEY --- */
akerr_ErrorContext *akbasic_cmd_getkey(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
akbasic_ASTLeaf *arg = NULL;
bool numeric = false;
bool available = false;
int keycode = 0;
(void)lval; (void)rval;
PASS(errctx, require_input(obj, "GETKEY"));
PASS(errctx, target_variable(obj, expr, "GETKEY", &variable, &numeric));
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
if ( available ) {
obj->input_state.waiting = false;
PASS(errctx, store_key(variable, numeric, keycode, true));
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
/*
* Nothing typed yet. A C128 would sit here; the library may not, so instead
* the step loop is told to stop advancing the program counter. The line
* re-executes on each step until a key turns up, which looks the same to the
* program and leaves the host its frame rate. TODO.md section 5.
*/
arg = akbasic_leaf_first_argument(expr);
if ( arg == NULL ) {
arg = expr->right;
}
obj->input_state.waiting = true;
obj->input_state.numeric = numeric;
strncpy(obj->input_state.variable, arg->identifier,
sizeof(obj->input_state.variable) - 1);
obj->input_state.variable[sizeof(obj->input_state.variable) - 1] = '\0';
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_input_service(akbasic_Runtime *obj, bool *blocked)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
bool available = false;
int keycode = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
"NULL argument in input_service");
*blocked = false;
if ( !obj->input_state.waiting ) {
SUCCEED_RETURN(errctx);
}
/*
* A host that takes the input device away while a GETKEY is holding would
* otherwise wedge the script forever. Release it instead -- the variable
* keeps whatever it had, which is the empty string.
*/
if ( obj->input == NULL ) {
obj->input_state.waiting = false;
SUCCEED_RETURN(errctx);
}
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
if ( !available ) {
*blocked = true;
SUCCEED_RETURN(errctx);
}
obj->input_state.waiting = false;
PASS(errctx, akbasic_environment_get(obj->environment, obj->input_state.variable, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
"GETKEY could not reach the variable %s", obj->input_state.variable);
PASS(errctx, store_key(variable, obj->input_state.numeric, keycode, true));
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- SCNCLR --- */
akerr_ErrorContext *akbasic_cmd_scnclr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
(void)expr; (void)lval; (void)rval;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in SCNCLR");
/*
* Clearing the text screen is the sink's job, not a device's -- the sink is
* where PRINT already goes, and it is the only thing that knows what a
* "screen" means for this host. The stdio sink treats it as a no-op, since
* clearing a pipe means nothing.
*/
PASS(errctx, obj->sink->clear(obj->sink));
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}

View File

@@ -54,6 +54,8 @@ static const akbasic_Verb VERBS[] = {
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
{ "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 },
{ "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_arglist, akbasic_cmd_graphic },
@@ -89,6 +91,7 @@ static const akbasic_Verb VERBS[] = {
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
{ "RUN", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_run },
{ "SCALE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_scale },
{ "SCNCLR", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_scnclr },
{ "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 },

View File

@@ -101,4 +101,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sound(struct akbasic_Runtime *obj
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tempo(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_vol(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
/* Group E console input verbs -- src/runtime_input.c */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_get(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_getkey(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_scnclr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
#endif // _AKBASIC_SRC_VERBS_H_