Files
akbasic/src/runtime_console.c
Tachikoma 4e7d2cff6c 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

274 lines
10 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);
}
/* ---------------------------------------------------------------- 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);
}