diff --git a/TODO.md b/TODO.md index 26e9ce2..f6db57d 100644 --- a/TODO.md +++ b/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 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 -- `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 diff --git a/src/sink_stdio.c b/src/sink_stdio.c index 232b5b5..e1714ac 100644 --- a/src/sink_stdio.c +++ b/src/sink_stdio.c @@ -100,5 +100,13 @@ akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_Stdio obj->writeln = stdio_writeln; obj->readline = stdio_readline; 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); } diff --git a/src/sink_tee.c b/src/sink_tee.c index abc9e78..0be6c53 100644 --- a/src/sink_tee.c +++ b/src/sink_tee.c @@ -115,6 +115,34 @@ static akerr_ErrorContext *tee_moveto(akbasic_TextSink *self, int col, int row) 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) { PREPARE_ERROR(errctx); @@ -141,6 +169,14 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink obj->writeln = tee_writeln; obj->readline = tee_readline; 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 * 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) ) { 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); } diff --git a/tests/sink_tee.c b/tests/sink_tee.c index 5e976a2..d2ecd06 100644 --- a/tests/sink_tee.c +++ b/tests/sink_tee.c @@ -38,6 +38,38 @@ static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self) 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) { akbasic_TextSink primary; @@ -45,6 +77,7 @@ int main(void) akbasic_TextSink mirror; akbasic_StdioSink mirrorstate; akbasic_TextSink broken; + akbasic_TextSink gridded; akbasic_TextSink tee; akbasic_TeeSink teestate; FILE *primaryout = NULL; @@ -106,6 +139,51 @@ int main(void) TEST_REQUIRE_STR(line, ""); 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 * line is worse than a refused one: the golden corpus compares whole files.