Port the standalone SDL frontend, and give the sink a line editor
An AKGL build of `basic` was a terminal program with unused SDL linked into it. It now opens the reference's 800x600 window, draws BASIC output in the Commodore font, pumps events, lets you type at it, and still mirrors every byte to stdout. The stdout mirror is a composing sink rather than a second write inside the interpreter, and lives in the core library where it needs no SDL. The line editor waits for a typed line by borrowing one frame at a time from the host, so nothing blocks and nothing owns an event loop it should not. The whole golden corpus now runs through the SDL binary as well as the stdio one, byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
src/sink_tee.c
Normal file
120
src/sink_tee.c
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @file sink_tee.c
|
||||
* @brief A text sink that writes to two others.
|
||||
*
|
||||
* The reference's Write() and Println() (basicruntime_graphics.go:140,148) put
|
||||
* every line on stdout *and* on an SDL surface. TODO.md section 1.5 turned that
|
||||
* pair of hardcoded calls into a vtable specifically so the interpreter would
|
||||
* never carry the second one, and section 3 item 5 says it again for the
|
||||
* standalone AKGL driver: do not put a hardcoded second write in the
|
||||
* interpreter. This is where the second write lives instead.
|
||||
*
|
||||
* No SDL and no libakgl. Composing two function-pointer records needs neither,
|
||||
* so this stays in the core library where the stdio-only suite can test it.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/sink.h>
|
||||
|
||||
/** @brief Fetch and validate the state behind a tee sink. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *tee_state(akbasic_TextSink *self, akbasic_TeeSink **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in tee sink");
|
||||
*dest = (akbasic_TeeSink *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
||||
"tee sink has no state");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *tee_write(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_TeeSink *state = NULL;
|
||||
|
||||
PASS(errctx, tee_state(self, &state));
|
||||
PASS(errctx, state->primary->write(state->primary, text));
|
||||
PASS(errctx, state->mirror->write(state->mirror, text));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *tee_writeln(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_TeeSink *state = NULL;
|
||||
|
||||
PASS(errctx, tee_state(self, &state));
|
||||
PASS(errctx, state->primary->writeln(state->primary, text));
|
||||
PASS(errctx, state->mirror->writeln(state->mirror, text));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *tee_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_TeeSink *state = NULL;
|
||||
|
||||
PASS(errctx, tee_state(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in tee sink readline");
|
||||
if ( state->reader == NULL ) {
|
||||
/*
|
||||
* A pair with no reader reports end of input rather than raising, which
|
||||
* is the contract sink.h states: running off the end is how RUNSTREAM
|
||||
* mode finishes normally and INPUT already handles it.
|
||||
*/
|
||||
if ( len > 0 ) {
|
||||
dest[0] = '\0';
|
||||
}
|
||||
*eof = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
PASS(errctx, state->reader->readline(state->reader, dest, len, eof));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *tee_clear(akbasic_TextSink *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_TeeSink *state = NULL;
|
||||
|
||||
PASS(errctx, tee_state(self, &state));
|
||||
PASS(errctx, state->primary->clear(state->primary));
|
||||
PASS(errctx, state->mirror->clear(state->mirror));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink *state, akbasic_TextSink *primary, akbasic_TextSink *mirror, akbasic_TextSink *reader)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in sink_init_tee");
|
||||
FAIL_ZERO_RETURN(errctx, (primary != NULL && mirror != NULL), AKERR_NULLPOINTER,
|
||||
"A tee sink needs two sinks to write to");
|
||||
/*
|
||||
* Refused rather than quietly accepted. A third sink here would read from
|
||||
* somewhere nothing writes to, which is the sort of wiring mistake that
|
||||
* shows up much later as a program that never sees its own input.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx, (reader == NULL || reader == primary || reader == mirror),
|
||||
AKBASIC_ERR_VALUE,
|
||||
"A tee sink's reader must be one of the two sinks it writes to");
|
||||
|
||||
state->primary = primary;
|
||||
state->mirror = mirror;
|
||||
state->reader = reader;
|
||||
|
||||
obj->self = state;
|
||||
obj->write = tee_write;
|
||||
obj->writeln = tee_writeln;
|
||||
obj->readline = tee_readline;
|
||||
obj->clear = tee_clear;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user