Land a character written past a short row's terminator
A grid row is a NUL-terminated string, and `putchar_at()` wrote the character, advanced and terminated -- so `CHAR 1, 40, 1, "#"` on an otherwise empty row stored the `#` at column 40 with `text[1][0]` still `'\0'`, and the render loop, which stops at the terminator, drew nothing at all. The silent nothing is the trap. The write succeeded, the cursor moved, the stdout mirror showed the character, and only the window stayed blank -- so the program looked right everywhere except where it mattered. The documented truncation on the way *back* is fine and is unaffected. `putchar_at()` now fills the gap with spaces before storing. **Padded there rather than in `sink_moveto()`**, which TODO.md proposed: this way `moveto` stays read-only -- the objection that entry raised against its own suggestion -- and a row is only ever padded when a character actually arrives. The pad fills from the terminator rather than replacing it. The buffer is not cleared between rows, so replacing only the terminator would expose the tail of whatever longer row used to be there: writing "ABCDEFGHIJ", truncating it to "ABCX", then writing at column 7 must give "ABCX Z" and not "ABCX FGZ". tests/akgl_backends.c asserts all three cases, and the middle one keeps the truncation pinned. TODO.md section 6 item 32, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
TODO.md
16
TODO.md
@@ -1982,8 +1982,20 @@ in either corpus runs for minutes, which is why the first of these had never bee
|
||||
in `examples/breakout/characters` does, and it is the one thing in that listing that will break
|
||||
on a different font or window. Two more `RGR()` indices would answer it.
|
||||
|
||||
32. **A character written past a short row's terminator is stored where nothing will render
|
||||
it.** A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
|
||||
32. ~~**A character written past a short row's terminator is stored where nothing will
|
||||
render it.**~~ **Done** -- `putchar_at()` (`src/sink_akgl.c`) fills the gap with spaces
|
||||
before storing, so a positioned write always lands.
|
||||
|
||||
**Padded in `putchar_at()` rather than in `sink_moveto()`**, which is the friendlier of
|
||||
the two options this entry offered and avoids its own objection: `moveto` stays
|
||||
read-only, and a row is only ever padded when a character actually arrives. The pad
|
||||
fills from the terminator rather than replacing it, because the buffer is not cleared
|
||||
between rows -- replacing only the terminator would expose the tail of whatever longer
|
||||
row used to be there, and `tests/akgl_backends.c` asserts exactly that case.
|
||||
|
||||
The documented truncation on the way back is unaffected and is asserted beside it.
|
||||
|
||||
The original report: A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
|
||||
writes the character, advances, and terminates -- so `CHAR 1, 40, 1, "#"` on an empty row
|
||||
stores `#` at column 40 with `text[1][0]` still `'\0'`, and `sink_akgl_render()` draws
|
||||
nothing at all. The same call on a row already 50 characters long draws the `#` and erases
|
||||
|
||||
@@ -18,7 +18,7 @@ for the reasoning in each case.
|
||||
| `BOX` | `BOX src, x1, y1, x2, y2 [,angle]` | Outline a rectangle, optionally rotated. |
|
||||
| `BSAVE` | `BSAVE "name", from, to` | Write a range of memory to a file. |
|
||||
| `CATALOG` | `CATALOG` | **Refused.** The other name for `DIRECTORY`. |
|
||||
| `CHAR` | `CHAR col, x, y, "text"` | Put text at a character cell. Needs a sink with a cursor. |
|
||||
| `CHAR` | `CHAR col, x, y, "text"` | Put text at a character cell. Needs a sink with a cursor. Terminates the row where it stops, so it erases whatever followed. |
|
||||
| `CIRCLE` | `CIRCLE src, x, y, rx, ry [,...]` | Draw an ellipse, arc or polygon. |
|
||||
| `CLR` | `CLR` | Drop every variable and function, keeping the program. |
|
||||
| `COLLECT` | `COLLECT` | **Refused.** Validates a disk's block allocation map. |
|
||||
|
||||
@@ -211,9 +211,9 @@ RETURN
|
||||
Now draw it — and here is the second rule that shapes this listing:
|
||||
|
||||
**`CHAR` terminates the row where it stops.** A row of the grid is a C string, so writing
|
||||
at column N erases everything from N+1 on, and writing *past* the terminator of a short
|
||||
row draws nothing at all. There is no way to poke one character into the middle of a row
|
||||
and leave the rest alone.
|
||||
at column N erases everything from N+1 on. Writing *past* the end of a short row is fine
|
||||
— the gap fills with spaces — but there is still no way to poke one character into the
|
||||
middle of a row and leave the rest of it alone.
|
||||
|
||||
So a row is rebuilt whole and written from column 0, in one `CHAR`:
|
||||
|
||||
@@ -732,7 +732,7 @@ game against:
|
||||
| Rule | Because | Where |
|
||||
|---|---|---|
|
||||
| `DIM` at the top, never in a loop | An array created inside a scope costs pool slots for good; 4096 ends the run. A scalar is free | Step 3 |
|
||||
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | Step 4 |
|
||||
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops, so a second write erases the tail of the first | Step 4 |
|
||||
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 |
|
||||
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 |
|
||||
| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 |
|
||||
|
||||
@@ -76,9 +76,23 @@ static void newline(akbasic_AkglSink *state)
|
||||
* program that PRINTs a long string and then PRINTs again expects the second one
|
||||
* to start on the row after the first one ended, and only the code that placed
|
||||
* the characters knows which row that is.
|
||||
*
|
||||
* **A row is a NUL-terminated string, so a write past its end pads.** Without
|
||||
* that, `CHAR 1, 40, 1, "#"` on an otherwise empty row stored the `#` at column
|
||||
* 40 with `text[1][0]` still `'\0'` -- and the render loop, which stops at the
|
||||
* terminator, drew nothing at all. The write succeeded, the cursor moved, the
|
||||
* stdout mirror showed the character, and the window stayed blank. That silent
|
||||
* nothing is the trap; the documented truncation on the way *back* is fine and
|
||||
* is unaffected. TODO.md section 6 item 32.
|
||||
*
|
||||
* The padding is spaces rather than whatever was there: the buffer is not
|
||||
* cleared between rows, so the gap holds the tail of some longer row that used
|
||||
* to be here.
|
||||
*/
|
||||
static void putchar_at(akbasic_AkglSink *state, char c)
|
||||
{
|
||||
int col = 0;
|
||||
|
||||
if ( c == '\n' ) {
|
||||
newline(state);
|
||||
return;
|
||||
@@ -86,6 +100,14 @@ static void putchar_at(akbasic_AkglSink *state, char c)
|
||||
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
||||
newline(state);
|
||||
}
|
||||
for ( col = 0; col < state->cursorcol; col++ ) {
|
||||
if ( state->text[state->cursorrow][col] == '\0' ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for ( ; col < state->cursorcol; col++ ) {
|
||||
state->text[state->cursorrow][col] = ' ';
|
||||
}
|
||||
state->text[state->cursorrow][state->cursorcol] = c;
|
||||
state->cursorcol += 1;
|
||||
state->text[state->cursorrow][state->cursorcol] = '\0';
|
||||
|
||||
@@ -305,6 +305,53 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_sink_grid_and_wrap(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A character placed past a short row's end pads rather than vanishing.
|
||||
*
|
||||
* A row is a NUL-terminated string, so `CHAR 1, 6, 0, "#"` on an otherwise empty
|
||||
* row used to store the `#` at column 6 with `text[0][0]` still `'\0'` -- and
|
||||
* the render loop, which stops at the terminator, drew nothing at all. The write
|
||||
* succeeded, the cursor moved, the stdout mirror showed the character, and the
|
||||
* window stayed blank. That silent nothing is the trap; the documented
|
||||
* truncation on the way back is fine and is asserted here too.
|
||||
*
|
||||
* The padding has to be spaces rather than whatever is in the buffer, because
|
||||
* the buffer is not cleared between rows: the gap holds the tail of some longer
|
||||
* row that used to be here, which is the second case below.
|
||||
*
|
||||
* TODO.md section 6 item 32.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_writes_past_a_short_row(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
|
||||
TARGET_SIZE, TARGET_SIZE));
|
||||
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 6, 0));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "#"));
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], " #");
|
||||
|
||||
/*
|
||||
* Writing back over a long row still truncates it -- that half is the
|
||||
* documented behaviour and the reason a program builds a row whole.
|
||||
*/
|
||||
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "ABCDEFGHIJ"));
|
||||
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 3, 0));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "X"));
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "ABCX");
|
||||
|
||||
/*
|
||||
* And a write past *that* terminator pads with spaces rather than exposing
|
||||
* the "EFGHIJ" still sitting in the buffer behind it. A pad that only
|
||||
* replaced the terminator itself would leave those visible.
|
||||
*/
|
||||
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 7, 0));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "Z"));
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "ABCX Z");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief The sink's text actually reaches the target when the host renders. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_renders(void)
|
||||
{
|
||||
@@ -562,6 +609,7 @@ int main(void)
|
||||
CATCH(errctx, test_paint_fills_region());
|
||||
CATCH(errctx, test_shape_roundtrip());
|
||||
CATCH(errctx, test_sink_grid_and_wrap());
|
||||
CATCH(errctx, test_sink_writes_past_a_short_row());
|
||||
CATCH(errctx, test_sink_renders());
|
||||
CATCH(errctx, test_input_backend());
|
||||
CATCH(errctx, test_sprite_from_file());
|
||||
|
||||
Reference in New Issue
Block a user