diff --git a/CMakeLists.txt b/CMakeLists.txt index 177b160..b06184a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ set(AKBASIC_SOURCES src/runtime_commands.c src/runtime_functions.c src/runtime_graphics.c + src/runtime_input.c src/scanner.c src/sink_stdio.c src/symtab.c @@ -197,6 +198,7 @@ set(AKBASIC_TESTS error_codes grammar_leaves graphics_verbs + input_verbs parser_commands parser_expressions runtime_evaluate diff --git a/TODO.md b/TODO.md index cd8960f..e26d109 100644 --- a/TODO.md +++ b/TODO.md @@ -424,12 +424,13 @@ Order by what unblocks the most, and by what does not need `libakgl` to grow fir | B. Housekeeping | `NEW`, `CLR`, `CONT`, `RESTORE`, `RENUMBER`, `SWAP`, `TRON`, `TROFF`, `HELP` | nothing | | C. Errors | `TRAP`, `RESUME`, `ER`, `ERR` | needs a BASIC-visible error object | | D. Strings/format | `USING`, `PUDEF`, `WIDTH`, `CHAR` | nothing | -| E. Console | `GET`, `GETKEY`, `SCNCLR`, `WINDOW`, `KEY`, `SLEEP`, `WAIT`, `TI` | `GET`/`GETKEY`/`SCNCLR` nothing; the rest want the sink or a clock | +| E. Console | `WINDOW`, `KEY`, `SLEEP`, `WAIT`, `TI` | the sink, or the clock `akbasic_runtime_settime()` now carries | | F. Disk | `DOPEN`, `DCLOSE`, `APPEND`, `RECORD`, `HEADER`, `COLLECT`, `BACKUP`, `COPY`, `CONCAT`, `RENAME`, `SCRATCH`, `DIRECTORY`, `CATALOG`, `DCLEAR`, `DVERIFY`, `SAVE`, `LOAD`, `VERIFY`, `BLOAD`, `BSAVE`, `BOOT` | `aksl_f*` only; no new akgl | -| G. Graphics | `GRAPHIC`, `DRAW`, `BOX`, `CIRCLE`, `PAINT`, `COLOR`, `SCALE`, `SSHAPE`, `GSHAPE`, `LOCATE` | nothing — `akgl_draw_*` landed at 42b60f7 | +| ~~G. Graphics~~ | ~~`GRAPHIC`, `DRAW`, `BOX`, `CIRCLE`, `PAINT`, `COLOR`, `SCALE`, `SSHAPE`, `GSHAPE`, `LOCATE`~~ | **done** — `src/runtime_graphics.c`, `tests/graphics_verbs.c` | | H. Sprites | `SPRITE`, `MOVSPR`, `SPRCOLOR`, `SPRDEF`, `SPRSAV`, `COLLISION` | `libakgl` sprite/actor API — check before filing | -| I. Audio | `PLAY`, `SOUND`, `ENVELOPE`, `VOL`, `TEMPO` | nothing — `akgl_audio_*` landed at 42b60f7 | -| I′. Audio, still gapped | `FILTER`, `SOUND`'s sweep arguments | `libakgl` has neither; see §7 | +| ~~I. Audio~~ | ~~`PLAY`, `SOUND`, `ENVELOPE`, `VOL`, `TEMPO`~~ | **done** — `src/runtime_audio.c`, `src/play.c`, `tests/audio_verbs.c` | +| I′. Audio, still gapped | `FILTER`, `SOUND`'s sweep arguments | `libakgl` has neither; both refused with `AKBASIC_ERR_DEVICE`, both filed in §7 | +| ~~E′. Console input~~ | ~~`GET`, `GETKEY`, `SCNCLR`~~ | **done** — `src/runtime_input.c`, `tests/input_verbs.c` | | J. Machine | `SYS`, `FETCH`, `STASH`, `POKE`/`PEEK` variants | nothing | `LOCATE` used to appear in both E and G. It belongs to G alone: in BASIC 7.0 `LOCATE` moves the @@ -552,6 +553,21 @@ behaviour to port. They matter to somebody typing in a listing out of a C128 man default-tempo quarter note at 500 ms — 120 beats per minute. If the real constant turns up, that is the one line to change. +23. **`GETKEY` holds the step loop rather than blocking.** A C128 sits on the keyboard inside + `GETKEY` until somebody types. §1.6 forbids the library blocking, so the verb sets a flag + and `akbasic_runtime_step()` declines to advance the program until a key arrives. Every + step still returns and a bounded `akbasic_runtime_run()` still comes back, so a host keeps + its frame rate; the program simply does not move past the `GETKEY`, which is the part a + program author cares about. The `PLAY` queue is serviced *before* this 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 forever. + +24. **`GET` and `GETKEY` into a numeric variable yield the key code.** BASIC 7.0's `GET` takes + a string variable. Accepting an integer one as well, and giving it the raw code, is what a + program testing for cursor or function keys needs and costs nothing. No key is code zero, + matching what a C128 reports. A float variable is refused: it is neither a character nor a + code. + --- ## 6. Reference defects — ported faithfully, fix them deliberately diff --git a/include/akbasic/input.h b/include/akbasic/input.h index 037f7fc..153cefc 100644 --- a/include/akbasic/input.h +++ b/include/akbasic/input.h @@ -39,4 +39,44 @@ typedef struct akbasic_InputBackend akerr_ErrorContext AKERR_NOIGNORE *(*flush_keys)(struct akbasic_InputBackend *self); } akbasic_InputBackend; +/** + * @brief The console input verbs' state, which lives on the runtime. + * + * `waiting` is what makes GETKEY work without blocking. On a C128, GETKEY sits + * on the keyboard until somebody types; section 1.6 forbids that, so instead the + * verb sets this and akbasic_runtime_step() declines to advance the program + * until a key turns up. A host keeps its frame rate, a bounded + * akbasic_runtime_run() still returns, and the program still does not move past + * the GETKEY -- which is the part the program author cares about. + */ +typedef struct +{ + bool waiting; /* a GETKEY is holding the program */ + char variable[64]; /* the variable it will assign the key to */ + bool numeric; /* true when that variable takes a code, not a letter */ +} akbasic_InputState; + +/* --- Internal API: exposed for the step loop and the tests. --- */ + +struct akbasic_Runtime; + +/** + * @brief Answer whether a GETKEY is still holding, assigning the key if one came. + * + * Called from akbasic_runtime_step() before it executes anything. While + * `*blocked` comes back true the step does nothing at all, so the program stays + * exactly where the GETKEY left it. + * + * Taking the input device away while a GETKEY is holding releases the hold + * rather than wedging the script forever -- a host is allowed to change its mind + * about what it lends out. + * + * @param obj Object to initialize, inspect, or modify. + * @param blocked Output destination populated by the function; true while a GETKEY is unsatisfied. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When either argument is NULL. + * @throws AKBASIC_ERR_UNDEFINED When the variable the GETKEY named has gone out of scope. + */ +akerr_ErrorContext AKERR_NOIGNORE *akbasic_input_service(struct akbasic_Runtime *obj, bool *blocked); + #endif // _AKBASIC_INPUT_H_ diff --git a/include/akbasic/runtime.h b/include/akbasic/runtime.h index 3fc08dd..bbb2652 100644 --- a/include/akbasic/runtime.h +++ b/include/akbasic/runtime.h @@ -111,6 +111,9 @@ typedef struct akbasic_Runtime */ akbasic_AudioState audio_state; + /* GETKEY's hold on the step loop. Same reasoning again: it is the program's. */ + akbasic_InputState input_state; + /* * The host's clock, in milliseconds, as of its last akbasic_runtime_settime() * call. The interpreter does not read a clock: it owns no loop and must not diff --git a/src/runtime.c b/src/runtime.c index ba0203e..699b16c 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -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)); diff --git a/src/runtime_input.c b/src/runtime_input.c new file mode 100644 index 0000000..beaa5c0 --- /dev/null +++ b/src/runtime_input.c @@ -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 + +#include + +#include +#include +#include + +#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); +} diff --git a/src/verbs.c b/src/verbs.c index f0219b4..73b23fa 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -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 }, diff --git a/src/verbs.h b/src/verbs.h index cacccf7..0bdfe71 100644 --- a/src/verbs.h +++ b/src/verbs.h @@ -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_ diff --git a/tests/input_verbs.c b/tests/input_verbs.c new file mode 100644 index 0000000..73774ce --- /dev/null +++ b/tests/input_verbs.c @@ -0,0 +1,197 @@ +/** + * @file input_verbs.c + * @brief Tests GET, GETKEY and SCNCLR against the recording mock backend. + * + * The interesting assertion in here is that GETKEY holds the program without + * blocking the caller. The test drives the step loop by hand and checks that the + * program counter does not move while the key buffer is empty, that the step + * still returns, and that the program resumes the moment a key turns up. + */ + +#include + +#include +#include +#include + +#include "harness.h" +#include "mockdevice.h" +#include "testutil.h" + +/** @brief Bring up a runtime with the mock devices attached and a program loaded. */ +static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source) +{ + PREPARE_ERROR(errctx); + + PASS(errctx, harness_start(NULL)); + mock_devices_init(); + PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, + &MOCK_AUDIO, &MOCK_INPUT)); + PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source)); + PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + SUCCEED_RETURN(errctx); +} + +/** + * @brief GET on an empty buffer yields the empty string and no error. + * + * This is the common case, not the edge case: it is what happens on most + * iterations of every GET loop ever written. + */ +static void test_get_empty(void) +{ + TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT \"[\" + A$ + \"]\"\n")); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "[]\n"); + harness_stop(); +} + +/** @brief GET takes one key per call, in the order they were typed. */ +static void test_get_drains_in_order(void) +{ + TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT A$\n30 GET A$\n40 PRINT A$\n")); + mock_push_keys("HI"); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "H\nI\n"); + harness_stop(); +} + +/** + * @brief A numeric variable receives the key code rather than the character. + * + * That is what a program testing for a cursor key or a function key wants, and + * no key is code zero -- which is what a C128 reports too. + */ +static void test_get_numeric(void) +{ + TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n")); + mock_push_keys("A"); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "65\n"); + harness_stop(); + + TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n")); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n"); + harness_stop(); +} + +/** + * @brief GETKEY holds the program without blocking the caller. + * + * The assertion that matters. A C128 sits on the keyboard inside GETKEY; here + * every step still returns, a bounded run() still comes back, and the program + * simply does not advance -- which is the part a program author cares about. + */ +static void test_getkey_holds_without_blocking(void) +{ + int i = 0; + + /* + * Lines 1 and 2 rather than the customary 10 and 20, because a step budget + * has to clear them. Source lines are stored indexed by line number and the + * cursor starts at zero, so a step spends itself on each empty slot on the + * way: a program at line 10 needs eleven steps before it has run anything. + * That is worth knowing before writing any bounded-run test. + */ + TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"GOT \" + A$\n")); + + /* + * A bounded run with an empty buffer returns rather than hanging, and the + * program has not reached line 2. If GETKEY blocked, this call would never + * come back and the test would time out instead of failing. + */ + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, ""); + TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting, + "GETKEY should be holding the program"); + + /* Any number of further steps changes nothing while the buffer is empty. */ + for ( i = 0; i < 10; i++ ) { + TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME)); + } + TEST_REQUIRE_STR(HARNESS_OUTPUT, ""); + + /* A keystroke releases it, and the program picks up where it left off. */ + mock_push_keys("X"); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT X\n"); + TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting, + "GETKEY should have released once a key arrived"); + harness_stop(); +} + +/** @brief A key already waiting satisfies GETKEY on the spot. */ +static void test_getkey_immediate(void) +{ + TEST_REQUIRE_OK(run_program("10 GETKEY A$\n20 PRINT \"GOT \" + A$\n")); + mock_push_keys("Z"); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT Z\n"); + TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting, + "GETKEY should not have held when a key was already waiting"); + harness_stop(); +} + +/** + * @brief Taking the device away mid-GETKEY releases the hold rather than wedging. + * + * A host is allowed to change its mind about what it lends out, and a script + * stuck forever on a device that no longer exists is the worst of the available + * outcomes. + */ +static void test_getkey_device_withdrawn(void) +{ + TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"RELEASED\"\n")); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20)); + TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting, "GETKEY should be holding"); + + TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "RELEASED\n"); + harness_stop(); +} + +/** @brief SCNCLR goes through the sink, so it needs no input device at all. */ +static void test_scnclr(void) +{ + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCNCLR\n20 PRINT \"AFTER\"\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + /* The stdio sink treats a clear as a no-op: clearing a pipe means nothing. */ + TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n"); + harness_stop(); +} + +/** @brief GET and GETKEY name themselves when there is no device, and check types. */ +static void test_errors(void) +{ + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GET A$\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GET needs an input device") != NULL, + "GET without a device should name itself, got \"%s\"", HARNESS_OUTPUT); + harness_stop(); + + /* A float variable is neither a character nor a key code. */ + TEST_REQUIRE_OK(run_program("10 GET A%\n")); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "string or integer variable") != NULL, + "GET into a float should be refused, got \"%s\"", HARNESS_OUTPUT); + harness_stop(); +} + +int main(void) +{ + test_get_empty(); + test_get_drains_in_order(); + test_get_numeric(); + test_getkey_holds_without_blocking(); + test_getkey_immediate(); + test_getkey_device_withdrawn(); + test_scnclr(); + test_errors(); + return akbasic_test_failures; +} diff --git a/tests/language/input/no_device.bas b/tests/language/input/no_device.bas new file mode 100644 index 0000000..2249292 --- /dev/null +++ b/tests/language/input/no_device.bas @@ -0,0 +1,5 @@ +10 REM SCNCLR goes through the text sink, which every runtime has, so it works +20 REM with no input device attached. GET needs one and must name itself. +30 SCNCLR +40 PRINT "SCNCLR OK" +50 GET A$ diff --git a/tests/language/input/no_device.txt b/tests/language/input/no_device.txt new file mode 100644 index 0000000..39c897b --- /dev/null +++ b/tests/language/input/no_device.txt @@ -0,0 +1,3 @@ +SCNCLR OK +? 50 : RUNTIME ERROR GET needs an input device and this runtime has none +