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:
2026-08-02 00:13:42 -04:00
parent 8594d8471d
commit a1dcfcacf3
5 changed files with 89 additions and 7 deletions

View File

@@ -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. |

View File

@@ -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 |