sk_repl_idle() auto-flush: implement Section V's "anything dirty? no? done" check

Makes blk_vm_flush_all() (block_words.c) non-static and declares it in
block_words.h -- it's already the entire implementation behind
SAVE-BUFFERS (block_word_save_buffers() is a one-line wrapper), so
sk_repl_idle() can call the exact same flush path outside word dispatch
without duplicating any logic. Cheap every idle tick regardless of dirty
state: every check inside is a small fixed-size scan, so no separate
pre-check was needed on top of it.

Caught a real bug via a live persistence test before trusting the
feature: the first version gated the flush on sk_repl_get_active_vm()
returning non-NULL, but NULL is that accessor's documented default
(Tripod's own USE-redirect override, "restore default dispatch") --
without an active USE redirect, the flush silently no-op'd for the
entire session. Confirmed live: wrote a byte via BUFFER (no
UPDATE/SAVE-BUFFERS), waited past the idle cadence, killed QEMU abruptly,
rebooted with the same disk image, read back 0 instead of the written
65. Fixed by threading the VM sk_repl_run()'s own loop already resolves
each iteration (g_repl_active_vm ? g_repl_active_vm : vm) down as a
parameter through sk_readline() into sk_repl_idle(), rather than trying
to re-derive it from an accessor with the wrong default. Re-ran the same
test after the fix: read back 65, matching the written byte -- the write
survived an abrupt kill with no explicit flush call anywhere in the
test, proving the idle-tick auto-flush genuinely ran.

All three architectures re-verified clean. FABRIC-2.md Section V item 6
and the corresponding Milestone 3 punch-list item marked done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
This commit is contained in:
Robert Allan James
2026-08-25 14:51:02 -04:00
co-authored by Claude Sonnet 5
parent 1b80609cb0
commit 8eaefeb9ee
20 changed files with 63371 additions and 11 deletions
+28 -5
View File
@@ -38,6 +38,7 @@
#include "starkernel/blkio_usb.h"
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
#include "word_source/include/block_words.h"
#include <stdint.h>
#include <string.h>
@@ -77,7 +78,7 @@ VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; }
static uint64_t g_last_beat_tick; /* zero-initialized (BSS) */
static void sk_repl_idle(void)
static void sk_repl_idle(VM *active_vm)
{
/* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the
* "interrupt-driven, coarse cadence, cheap early-exit" trigger Section
@@ -124,6 +125,28 @@ static void sk_repl_idle(void)
xdev->bot_msc_detach_pending = 0;
blk_subsys_detach_device(&usb_blk_dev);
}
/* FABRIC.md/FABRIC-2.md Section V item 6: "a cheap 'anything dirty?
* no? done' block-sync check", the same "interrupt-driven, coarse
* cadence, cheap early-exit" trigger shape as the xHCI servicing
* above -- this was the one piece of that design already fully
* specified and waiting for this hook to actually be non-empty.
* blk_vm_flush_all() (block_words.c, the same code SAVE-BUFFERS
* itself runs) is cheap to call when nothing is dirty -- every
* check inside is a small fixed-size scan, no disk I/O happens
* unless something genuinely needs writing -- so no separate
* "is anything dirty" pre-check is needed here.
*
* active_vm is passed in by the caller (sk_readline(), itself passed
* through from sk_repl_run()/sk_repl_step()'s own already-resolved
* VM) rather than read via sk_repl_get_active_vm() here -- that
* accessor returns NULL whenever Tripod's USE word hasn't redirected
* it, which is the common case, not "no VM is active." An earlier
* version of this code called sk_repl_get_active_vm() directly and
* silently no-op'd for exactly that reason, confirmed live: a BUFFER
* write with no UPDATE, followed by an idle wait and an abrupt kill,
* did not survive a reboot until this fix. */
blk_vm_flush_all(active_vm);
}
/*===========================================================================
@@ -231,7 +254,7 @@ static int sk_kbd_getc(void)
* Returns the number of characters placed in buf (not counting '\0').
*===========================================================================*/
static int sk_readline(char *buf, int size)
static int sk_readline(char *buf, int size, VM *active_vm)
{
int n = 0;
@@ -255,7 +278,7 @@ static int sk_readline(char *buf, int size)
uint64_t now = heartbeat_ticks();
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle();
sk_repl_idle(active_vm);
}
/*
* Do NOT use hlt here: QEMU single-threaded TCG can't process
@@ -350,7 +373,7 @@ int sk_repl_step(VM *vm)
console_puts(SK_PROMPT_TEXT);
}
sk_readline(input, sizeof(input));
sk_readline(input, sizeof(input), vm);
if (input[0] == '\0') {
console_puts(" ok\n");
@@ -397,7 +420,7 @@ void sk_repl_run(VM *vm)
console_puts(SK_PROMPT_TEXT);
}
sk_readline(input, sizeof(input));
sk_readline(input, sizeof(input), active);
if (input[0] == '\0') {
console_puts(" ok\n");