Forward WINDOW through the tee sink
`akbasic_sink_init_tee()` wired write, writeln, readline, clear and -- conditionally -- moveto, and never `window`. The standalone AKGL frontend runs the interpreter against a tee, so the fully implemented `sink_window()` underneath it was unreachable: `WINDOW 0, 0, 20, 4` answered "WINDOW needs a text device with a character grid, and this one has none" on the one build that has a character grid. It reads as an oversight rather than a decision, because `moveto` directly above it is forwarded with reasoning that applies unchanged. `tee_window()` is that function, modelled on `tee_moveto()`, offered only when a half can take it so the refusal still reads correctly through a stdio-only pair. **Writing the test found a second one.** Both optional entry points are assigned conditionally, and neither initializer cleared them first -- so a caller with a sink on the stack got whatever was in that memory, and `CHAR` and `WINDOW` decide whether they can act by testing those pointers for NULL. Both `akbasic_sink_init_tee()` and `akbasic_sink_init_stdio()` now clear them. An initializer that leaves a field alone is not an initializer. tests/sink_tee.c gains a stand-in sink with a grid -- the two stdio halves have neither entry point, which is exactly why they could not show that either is forwarded -- and asserts both directions, the arguments arriving intact, and that a grid-less pair still offers neither. Verified end to end: `WINDOW 0, 0, 20, 4` then `PRINT` succeeds under build-akgl/basic and still refuses by name under build/basic. What this does *not* fix is that a drawing still has to be re-issued every frame: the frontend never clears and SDL is double-buffered, so a drawing issued once appears in one buffer only. Shrinking the text area makes the rest of the window the program's; keeping something there is still the program's job. That half is documented rather than fixed, and is filed as TODO.md section 9 item 5. TODO.md section 6 item 31, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
TODO.md
15
TODO.md
@@ -1932,7 +1932,20 @@ in either corpus runs for minutes, which is why the first of these had never bee
|
|||||||
has to be settled with it: while the pool is dry the failure cannot be trapped, so a
|
has to be settled with it: while the pool is dry the failure cannot be trapped, so a
|
||||||
program cannot even report this one against itself.
|
program cannot even report this one against itself.
|
||||||
|
|
||||||
31. **`WINDOW` cannot be reached in the standalone AKGL build.** `akbasic_sink_init_tee()`
|
31. ~~**`WINDOW` cannot be reached in the standalone AKGL build.**~~ **Done** — `tee_window()`
|
||||||
|
(`src/sink_tee.c`) forwards to whichever half offers one, offered only when one does,
|
||||||
|
with the case in `tests/sink_tee.c` beside the `moveto` one. `WINDOW 0, 0, 20, 4` now
|
||||||
|
works in the windowed build and still refuses by name without a grid.
|
||||||
|
|
||||||
|
**`akbasic_sink_init_tee()` and `akbasic_sink_init_stdio()` also now clear `moveto` and
|
||||||
|
`window` rather than leaving them alone.** Writing the test found it: both are
|
||||||
|
conditional in the tee, so a caller with a sink on the stack got whatever was in that
|
||||||
|
memory, and `CHAR` decides whether it can move a cursor by testing that pointer for
|
||||||
|
NULL. An initializer that leaves a field alone is not an initializer.
|
||||||
|
|
||||||
|
The grid-size half of this item is closed separately; see item 31a below.
|
||||||
|
|
||||||
|
The original report: `akbasic_sink_init_tee()`
|
||||||
(`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
|
(`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
|
||||||
-- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
|
-- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
|
||||||
the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
|
the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
|
||||||
|
|||||||
@@ -100,5 +100,13 @@ akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_Stdio
|
|||||||
obj->writeln = stdio_writeln;
|
obj->writeln = stdio_writeln;
|
||||||
obj->readline = stdio_readline;
|
obj->readline = stdio_readline;
|
||||||
obj->clear = stdio_clear;
|
obj->clear = stdio_clear;
|
||||||
|
/*
|
||||||
|
* The optional entry points, cleared rather than left alone. A stream has no
|
||||||
|
* character grid, so `CHAR` and `WINDOW` must refuse by name -- and they
|
||||||
|
* decide that by testing the pointer for NULL. A caller with a sink on the
|
||||||
|
* stack would otherwise hand them whatever was in that memory.
|
||||||
|
*/
|
||||||
|
obj->moveto = NULL;
|
||||||
|
obj->window = NULL;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,34 @@ static akerr_ErrorContext *tee_moveto(akbasic_TextSink *self, int col, int row)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constrain whichever half has a grid, for WINDOW.
|
||||||
|
*
|
||||||
|
* Same shape as tee_moveto() above it, and for the same reason: a stdio half has
|
||||||
|
* no grid to constrain and a windowed half does, so the call goes to whoever can
|
||||||
|
* take it and the other is left alone.
|
||||||
|
*
|
||||||
|
* This was missing, and its absence made `WINDOW` unreachable in the one build
|
||||||
|
* that has a character grid -- the standalone AKGL frontend runs the interpreter
|
||||||
|
* against a tee, so the fully implemented sink_window() underneath answered
|
||||||
|
* nothing. `WINDOW` refused with "needs a text device with a character grid" on
|
||||||
|
* the build that had one. TODO.md section 6 item 31.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_TeeSink *state = NULL;
|
||||||
|
|
||||||
|
PASS(errctx, tee_state(self, &state));
|
||||||
|
if ( state->primary != NULL && state->primary->window != NULL ) {
|
||||||
|
PASS(errctx, state->primary->window(state->primary, left, top, right, bottom));
|
||||||
|
}
|
||||||
|
if ( state->mirror != NULL && state->mirror->window != NULL ) {
|
||||||
|
PASS(errctx, state->mirror->window(state->mirror, left, top, right, bottom));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink *state, akbasic_TextSink *primary, akbasic_TextSink *mirror, akbasic_TextSink *reader)
|
akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink *state, akbasic_TextSink *primary, akbasic_TextSink *mirror, akbasic_TextSink *reader)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -141,6 +169,14 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
|
|||||||
obj->writeln = tee_writeln;
|
obj->writeln = tee_writeln;
|
||||||
obj->readline = tee_readline;
|
obj->readline = tee_readline;
|
||||||
obj->clear = tee_clear;
|
obj->clear = tee_clear;
|
||||||
|
/*
|
||||||
|
* Cleared first, because the two below are conditional and an initializer
|
||||||
|
* that leaves a field alone is not an initializer: a caller with a sink on
|
||||||
|
* the stack would get whatever was in that memory, and `CHAR` decides
|
||||||
|
* whether it can move a cursor by testing this pointer for NULL.
|
||||||
|
*/
|
||||||
|
obj->moveto = NULL;
|
||||||
|
obj->window = NULL;
|
||||||
/*
|
/*
|
||||||
* Offered only when a half can actually do it, so CHAR's refusal against a
|
* Offered only when a half can actually do it, so CHAR's refusal against a
|
||||||
* stdio-only driver still reads correctly through a tee.
|
* stdio-only driver still reads correctly through a tee.
|
||||||
@@ -149,5 +185,11 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
|
|||||||
(mirror != NULL && mirror->moveto != NULL) ) {
|
(mirror != NULL && mirror->moveto != NULL) ) {
|
||||||
obj->moveto = tee_moveto;
|
obj->moveto = tee_moveto;
|
||||||
}
|
}
|
||||||
|
/* Same rule for WINDOW, so its refusal against a grid-less pair still reads
|
||||||
|
correctly through a tee. */
|
||||||
|
if ( (primary != NULL && primary->window != NULL) ||
|
||||||
|
(mirror != NULL && mirror->window != NULL) ) {
|
||||||
|
obj->window = tee_window;
|
||||||
|
}
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,38 @@ static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self)
|
|||||||
FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
|
FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A half that has a character grid. The two stdio sinks above have neither
|
||||||
|
* `moveto` nor `window`, which is exactly why they cannot show that either one
|
||||||
|
* is forwarded -- so this stands in for the akgl sink, without the SDL this file
|
||||||
|
* deliberately does not link.
|
||||||
|
*/
|
||||||
|
static int MOVETOS = 0;
|
||||||
|
static int WINDOWS = 0;
|
||||||
|
static int WINDOW_ARGS[4] = { 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
(void)self; (void)col; (void)row;
|
||||||
|
MOVETOS += 1;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
(void)self;
|
||||||
|
WINDOWS += 1;
|
||||||
|
WINDOW_ARGS[0] = left;
|
||||||
|
WINDOW_ARGS[1] = top;
|
||||||
|
WINDOW_ARGS[2] = right;
|
||||||
|
WINDOW_ARGS[3] = bottom;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
akbasic_TextSink primary;
|
akbasic_TextSink primary;
|
||||||
@@ -45,6 +77,7 @@ int main(void)
|
|||||||
akbasic_TextSink mirror;
|
akbasic_TextSink mirror;
|
||||||
akbasic_StdioSink mirrorstate;
|
akbasic_StdioSink mirrorstate;
|
||||||
akbasic_TextSink broken;
|
akbasic_TextSink broken;
|
||||||
|
akbasic_TextSink gridded;
|
||||||
akbasic_TextSink tee;
|
akbasic_TextSink tee;
|
||||||
akbasic_TeeSink teestate;
|
akbasic_TeeSink teestate;
|
||||||
FILE *primaryout = NULL;
|
FILE *primaryout = NULL;
|
||||||
@@ -106,6 +139,51 @@ int main(void)
|
|||||||
TEST_REQUIRE_STR(line, "");
|
TEST_REQUIRE_STR(line, "");
|
||||||
TEST_REQUIRE_STATUS(tee.readline(&tee, NULL, sizeof(line), &eof), AKERR_NULLPOINTER);
|
TEST_REQUIRE_STATUS(tee.readline(&tee, NULL, sizeof(line), &eof), AKERR_NULLPOINTER);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `moveto` and `window` are offered only when a half can do them, so that a
|
||||||
|
* `CHAR` or a `WINDOW` against a stdio-only pair still refuses by name
|
||||||
|
* rather than appearing to work. Two stdio halves have neither.
|
||||||
|
*/
|
||||||
|
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, NULL));
|
||||||
|
TEST_REQUIRE(tee.moveto == NULL, "a tee over two grid-less sinks must not offer moveto");
|
||||||
|
TEST_REQUIRE(tee.window == NULL, "a tee over two grid-less sinks must not offer window");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And when a half *does* have them, they are forwarded. `window` used to be
|
||||||
|
* the one entry point this function never wired, which made WINDOW
|
||||||
|
* unreachable in the only build that has a character grid: the standalone
|
||||||
|
* frontend runs the interpreter against a tee, so the akgl sink's fully
|
||||||
|
* implemented window() answered nothing and the verb refused with "needs a
|
||||||
|
* text device with a character grid". TODO.md section 6 item 31.
|
||||||
|
*/
|
||||||
|
gridded.self = &gridded;
|
||||||
|
gridded.write = broken_write;
|
||||||
|
gridded.writeln = broken_write;
|
||||||
|
gridded.readline = NULL;
|
||||||
|
gridded.clear = broken_clear;
|
||||||
|
gridded.moveto = counting_moveto;
|
||||||
|
gridded.window = counting_window;
|
||||||
|
MOVETOS = 0;
|
||||||
|
WINDOWS = 0;
|
||||||
|
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL));
|
||||||
|
TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto");
|
||||||
|
TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window");
|
||||||
|
TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4));
|
||||||
|
TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24));
|
||||||
|
TEST_REQUIRE_INT(MOVETOS, 1);
|
||||||
|
TEST_REQUIRE_INT(WINDOWS, 1);
|
||||||
|
TEST_REQUIRE_INT(WINDOW_ARGS[0], 0);
|
||||||
|
TEST_REQUIRE_INT(WINDOW_ARGS[1], 20);
|
||||||
|
TEST_REQUIRE_INT(WINDOW_ARGS[2], 39);
|
||||||
|
TEST_REQUIRE_INT(WINDOW_ARGS[3], 24);
|
||||||
|
|
||||||
|
/* The gridded half being first is wired the same way round. */
|
||||||
|
WINDOWS = 0;
|
||||||
|
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &gridded, &primary, NULL));
|
||||||
|
TEST_REQUIRE(tee.window != NULL, "the gridded half may be either one");
|
||||||
|
TEST_REQUIRE_OK(tee.window(&tee, 1, 2, 3, 4));
|
||||||
|
TEST_REQUIRE_INT(WINDOWS, 1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A failing half stops the pair rather than being swallowed. A half-written
|
* A failing half stops the pair rather than being swallowed. A half-written
|
||||||
* line is worse than a refused one: the golden corpus compares whole files.
|
* line is worse than a refused one: the golden corpus compares whole files.
|
||||||
|
|||||||
Reference in New Issue
Block a user