Files
akbasic/src/runtime_console.c
Tachikoma f626d7985f Let a program ask how big the text grid is
`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows
or the cell size -- so anything placing a character *and* a sprite at the same
spot had to hardcode a number measured by hand against whatever font the host
loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the
one thing in that listing that breaks on a different font or window.

**`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.**
`RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns.
`RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter
has neither -- refused by name, because 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)`, beside the surface's own
dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about
the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a
column count stops being right the moment a program calls `WINDOW`.

Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns,
rows, cell width, cell height -- implemented by the akgl sink and forwarded by
the tee, in the shape `moveto` and `window` already had. NULL everywhere else,
so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_
stdio()` clears it for the same reason it now clears the other two.

Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50
columns by 37 rows -- the three numbers the Breakout listing had written out as
constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not.

tests/console_verbs.c drives the answers through a stand-in sink with a grid,
since the harness sink is stdio and has none; tests/graphics_verbs.c covers the
new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=`
block in docs/10-embedding.md moves with the header, which is `docs_examples`
doing its job.

TODO.md section 6 item 31's second half, struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 00:05:17 -04:00

333 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 <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 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++ ) {
snprintf(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");
snprintf(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");
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];
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;
snprintf(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);
}