Files
akbasic/src/runtime_console.c

333 lines
13 KiB
C
Raw Normal View History

Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:37 -04:00
/**
* @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);
}
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
/* --------------------------------------------------------------- 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);
}
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:37 -04:00
/* ---------------------------------------------------------------- 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);
}