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>
337 lines
13 KiB
C
337 lines
13 KiB
C
/**
|
|
* @file runtime_console.c
|
|
* @brief The group E verbs: SLEEP, WAIT, KEY, WINDOW, and the TI clock.
|
|
*
|
|
* Everything here that waits does so by *holding the step loop*, the way GETKEY
|
|
* already does (see akbasic_input_service). Section 1.6 forbids the library
|
|
* blocking: a host calls akbasic_runtime_step() once a frame and it must always
|
|
* come back, so "wait" means "do not advance this step" rather than "do not
|
|
* return". A bounded akbasic_runtime_run() still returns on time, and a host
|
|
* that wants to abandon the wait can change the mode.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
#include <akstdlib.h>
|
|
|
|
#include <akbasic/args.h>
|
|
#include <akbasic/error.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 Jiffies per second. A Commodore counts time in sixtieths. */
|
|
#define JIFFIES_PER_SECOND 60
|
|
|
|
/* ------------------------------------------------------------------ SLEEP -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sleep(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[1];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SLEEP");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SLEEP", args, 1, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "Expected SLEEP (seconds)");
|
|
FAIL_ZERO_RETURN(errctx, (args[0] >= 0.0), AKBASIC_ERR_VALUE,
|
|
"SLEEP cannot wait a negative number of seconds");
|
|
|
|
/*
|
|
* Records when to stop and returns. akbasic_console_service() holds the step
|
|
* loop until then -- so a program that sleeps still lets its host draw
|
|
* frames, and a sleeping program in an embedded game does not freeze the
|
|
* game.
|
|
*
|
|
* A host that never calls akbasic_runtime_settime() leaves the clock at
|
|
* zero, and a deadline computed from a clock that never advances is never
|
|
* reached. So no clock means no sleep: the verb does nothing rather than
|
|
* hanging the program, which is the same trade the audio durations make
|
|
* (§5 deviation 20) and the same direction -- fail fast, not silently
|
|
* forever.
|
|
*/
|
|
if ( obj->timems <= 0 ) {
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
obj->console_state.sleepuntilms = obj->timems + (int64_t)(args[0] * 1000.0);
|
|
obj->console_state.sleeping = true;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- WAIT -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_wait(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[3];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in WAIT");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "WAIT", args, 3, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
|
|
"Expected WAIT (address), (mask) [, (xor)]");
|
|
|
|
/*
|
|
* WAIT polls a byte until `(PEEK(addr) XOR xor) AND mask` is non-zero. On a
|
|
* C128 that byte is a hardware register an interrupt is changing; here it is
|
|
* ordinary process memory, and the only thing that can change it is the host
|
|
* -- or another thread, or a POKE from a TRAP handler.
|
|
*
|
|
* So this is honest but rarely useful: a program that waits on memory
|
|
* nothing writes waits forever, which is exactly what the same program does
|
|
* on a C128 with the wrong address. It holds the step loop rather than
|
|
* blocking, so the host keeps its frame rate and can stop the program.
|
|
*/
|
|
obj->console_state.waitaddress = (uintptr_t)args[0];
|
|
obj->console_state.waitmask = (uint8_t)args[1];
|
|
obj->console_state.waitxor = (count >= 3 ? (uint8_t)args[2] : 0);
|
|
obj->console_state.waiting = true;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------- KEY -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Value *value = NULL;
|
|
/* Room for the longest macro plus `KEY n, ""` around it. */
|
|
char line[AKBASIC_MAX_STRING_LENGTH + 32];
|
|
int64_t number = 0;
|
|
int written = 0;
|
|
int i = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in KEY");
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
if ( arg == NULL ) {
|
|
/* Bare KEY lists the definitions, which is what a C128 does. */
|
|
for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) {
|
|
PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "KEY %d, \"%s\"",
|
|
i + 1, obj->console_state.keys[i]));
|
|
PASS(errctx, akbasic_runtime_println(obj, line));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"KEY expected a key number");
|
|
number = value->intval;
|
|
FAIL_ZERO_RETURN(errctx, (number >= 1 && number <= AKBASIC_MAX_FUNCTION_KEYS),
|
|
AKBASIC_ERR_BOUNDS, "KEY %" PRId64 " is outside 1..%d",
|
|
number, AKBASIC_MAX_FUNCTION_KEYS);
|
|
|
|
arg = arg->next;
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected KEY (number), (string)");
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"KEY expected a string");
|
|
PASS(errctx, aksl_snprintf(&written, obj->console_state.keys[number - 1],
|
|
sizeof(obj->console_state.keys[0]), "%s", value->stringval));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- WINDOW -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_window(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[5];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in WINDOW");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "WINDOW", args, 5, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 4), AKBASIC_ERR_SYNTAX,
|
|
"Expected WINDOW (left), (top), (right), (bottom) [, (clear)]");
|
|
/*
|
|
* The geometry first, and deliberately: an inside-out rectangle is a mistake
|
|
* in the program whether or not a device is attached, and reporting "no
|
|
* device" for it would send the author looking in the wrong place.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (args[0] <= args[2] && args[1] <= args[3]), AKBASIC_ERR_VALUE,
|
|
"WINDOW's bottom right must not be above or left of its top left");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
|
|
"WINDOW needs a text device and this runtime has none");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink->window != NULL), AKBASIC_ERR_DEVICE,
|
|
"WINDOW needs a text device with a character grid, and this one has none");
|
|
|
|
PASS(errctx, obj->sink->window(obj->sink, (int)args[0], (int)args[1],
|
|
(int)args[2], (int)args[3]));
|
|
if ( count >= 5 && args[4] != 0.0 ) {
|
|
PASS(errctx, obj->sink->clear(obj->sink));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- RWINDOW -- */
|
|
|
|
/**
|
|
* @brief How big the current text window is, in characters.
|
|
*
|
|
* BASIC 7.0's own answer to the question, and it had never been implemented
|
|
* here: `RWINDOW(0)` is the number of lines and `RWINDOW(1)` the number of
|
|
* columns. Without it a program had no way to find out, so anything placing a
|
|
* character *and* a sprite at the same spot had to hardcode a cell size measured
|
|
* against whatever font the host loaded. The Breakout in `examples/` did exactly
|
|
* that, and it was the one thing in that listing that broke on a different
|
|
* window. TODO.md section 6 item 31.
|
|
*
|
|
* **`RWINDOW(2)` is refused by name.** On a C128 it answers 0 for 40-column mode
|
|
* and 1 for 80-column, and this interpreter has no such mode -- the grid is
|
|
* whatever the window divided by the font gives. Answering 0 would be a
|
|
* plausible lie, which is worse than a refusal that says why.
|
|
*
|
|
* The cell size in pixels is `RGR(3)` and `RGR(4)` rather than a field here: it
|
|
* is a question about the drawing surface, and it belongs beside the surface's
|
|
* own dimensions.
|
|
*/
|
|
akerr_ErrorContext *akbasic_fn_rwindow(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Value *out = NULL;
|
|
double args[1];
|
|
int count = 0;
|
|
int field = 0;
|
|
int columns = 0;
|
|
int rows = 0;
|
|
int cellw = 0;
|
|
int cellh = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RWINDOW");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "RWINDOW", args, 1, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RWINDOW expected a field number");
|
|
field = (int)args[0];
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (field == 2), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW(2) reports a C128's 40 or 80 column mode, and this interpreter has neither");
|
|
FAIL_ZERO_RETURN(errctx, (field == 0 || field == 1), AKBASIC_ERR_BOUNDS,
|
|
"RWINDOW: field %d is outside 0..1", field);
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW needs a text device and this runtime has none");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink->grid != NULL), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW needs a text device with a character grid, and this one has none");
|
|
PASS(errctx, obj->sink->grid(obj->sink, &columns, &rows, &cellw, &cellh));
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
|
PASS(errctx, akbasic_value_zero(out));
|
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
|
out->intval = (field == 0 ? (int64_t)rows : (int64_t)columns);
|
|
*dest = out;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- service -- */
|
|
|
|
akerr_ErrorContext *akbasic_console_state_init(akbasic_ConsoleState *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init");
|
|
PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_console_service(akbasic_Runtime *obj, bool *blocked)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in console_service");
|
|
*blocked = false;
|
|
|
|
if ( obj->console_state.sleeping ) {
|
|
if ( obj->timems >= obj->console_state.sleepuntilms ) {
|
|
obj->console_state.sleeping = false;
|
|
} else {
|
|
*blocked = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
}
|
|
|
|
if ( obj->console_state.waiting ) {
|
|
const volatile uint8_t *address = (const volatile uint8_t *)obj->console_state.waitaddress;
|
|
uint8_t byte = 0;
|
|
|
|
/*
|
|
* `volatile`, because the whole point is that something outside this
|
|
* program changes it. Without it the compiler is entitled to read the
|
|
* byte once and spin on the register.
|
|
*/
|
|
byte = *address;
|
|
if ( ((byte ^ obj->console_state.waitxor) & obj->console_state.waitmask) != 0 ) {
|
|
obj->console_state.waiting = false;
|
|
} else {
|
|
*blocked = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Variable *variable = NULL;
|
|
int64_t zerosubscript[1] = { 0 };
|
|
int64_t jiffies = 0;
|
|
int64_t seconds = 0;
|
|
char text[16];
|
|
int written = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock");
|
|
if ( obj->environment == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* `TI` and `TI$` are `TI#` and `TI$` here, and they are written rather than
|
|
* computed on read: this dialect has no bare variable names and no
|
|
* pseudo-variable mechanism, so they are ordinary globals refreshed once per
|
|
* step. Same decision as `ER#` and `EL#`, for the same reason.
|
|
*
|
|
* Counted in jiffies -- sixtieths of a second -- from the host's clock,
|
|
* which is what `TI` means on a Commodore. A host that never calls
|
|
* akbasic_runtime_settime() leaves both at zero, which is a stopped clock
|
|
* rather than a wrong one.
|
|
*/
|
|
jiffies = (obj->timems * JIFFIES_PER_SECOND) / 1000;
|
|
PASS(errctx, akbasic_runtime_global(obj, "TI#", &variable));
|
|
PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1));
|
|
|
|
seconds = obj->timems / 1000;
|
|
PASS(errctx, aksl_snprintf(&written, text, sizeof(text),
|
|
"%02" PRId64 "%02" PRId64 "%02" PRId64,
|
|
(seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60));
|
|
PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable));
|
|
PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|