Cert storage expanded from the old 16-byte placeholder to a real
32-byte seed + 32-byte pubkey. vm_zuse_cert_install() now has a
kernel-side duplicate in src/starkernel/vm/vm_core.c -- the kernel
build's VM_EXCLUDE list drops src/vm.c entirely (same reason
vm_set_base() already has two independent copies), so the hosted-only
version added earlier this session was never actually linked into the
kernel. FORTH-side ZUSE-CERT-LO@/HI@ replaced with ZUSE-PUBKEY@ (i -- u)
over the public half only; ACL-ZUSE-BOOT now checks
ZUSE-CERT-INSTALLED? before authenticating instead of unconditionally.
Attempted NVRAM-based persistence (GetVariable/SetVariable) for the
first-boot mint flow: page-faulted inside OVMF's variable service
(CR2 in the flash MMIO window). Moving the call site to match the one
proven-safe existing SetVariable call site in this codebase produced
the identical crash -- not a timing issue. Localized with debug
markers (one boot): GetVariable works; SetVariable with real data
never returns. The existing "working" precedent call is actually a
delete-of-nonexistent-variable (size=0, data=NULL), a cheaper path
that never touches flash, so it proved nothing about real writes.
Root cause: this kernel's VMM never maps the region OVMF's variable
service needs for real flash writes -- a genuine gap in UEFI runtime-
services support, not Zuse-specific, and not obviously fixable in a
3-arch-uniform way (flash window location is firmware/arch-specific).
Independently, storing the raw seed in RUNTIME_ACCESS NVRAM would have
been a real security defect regardless of the crash -- readable by any
later-loaded UEFI app or the booted OS.
Reverted to a known-safe state: all NVRAM/mint code removed from
kernel_main.c, init.4th's ACL.4th line back to its documented
commented-out default. Verified clean compile and clean boot on all
three architectures. Cert storage expansion (the part that works)
stays. A dedicated system-identity disk (virtio-blk, already proven
for writes via Artemis) is the recommended next substrate -- not yet
decided or built. Full investigation documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Found that a pinned CONSTANT is not actually tamper-proof: ACL-PIN only
blocks redefinition, not a >BODY-then-store on the word's existing data
field. Moves the Zuse cert value into C-only VM struct fields
(zuse_cert_lo/hi + zuse_cert_installed fuse bit) with a one-time
vm_zuse_cert_install() and read-only ZUSE-CERT-LO@/HI@/INSTALLED? FORTH
accessors, closing the tamper path structurally instead of by convention.
Deletes the now-insecure ZUSE-CERT-LO/HI CONSTANT words from zuse.4th.
vm_zuse_cert_install() has no caller yet -- the real mint flow (Milestone
6 CA, the MINT word) is still open; this is storage + accessors only, not
a stand-in mint. Documented in FABRIC-3.md. Verified: hosted build clean,
mkcapsule --lint clean (31/31), clean boot to ok> on amd64/aarch64/riscv64
with Stadium conservation intact and no panics.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
stadium_admit()'s mass==1 refusal looked like a hard blocker for 1024-byte
blocks, but stadium_word_dispatch()'s real candidate construction proves
Stadium cells carry pure identity/heat/bookkeeping, never the resident's
actual content -- a block patron follows the same shape (identity=LBN,
payload unused), so this was real, scoped work, not a case for stubbing.
New stadium_blocks.h/.c mirror stadium_words.c's admission/cooling shape,
keyed by (quota_slot, lbn) in a fixed-capacity open-addressing hash table
(tombstone deletion) instead of a dense array, since LBN space isn't
densely bounded like word_id. Wired into block_word_block()/buffer()/
update() (block_words.c), __STARKERNEL__-guarded. stadium_dispatch()'s
MIGRATE case now calls blk_flush(lbn) for real instead of printing
"(stub)". Three new Kconfig constants (STADIUM_BLOCK_HEAT_QUANTUM/
STADIUM_BLOCK_COOL_RATE_Q48/STADIUM_BLOCK_TRACK_CAP_MULT) mirror the
word-patron ones, same three-layer wiring.
VM-COOL/DELIVER/EXPIRE stay explicit punch-list items -- VM-COOL
deferred pending the still-iterating Tripod/Zuse/messaging vision,
DELIVER/EXPIRE are their own future subsystem integrations per
FABRIC.md's own "open, not resolved" notes.
Verified clean compile (zero warnings) and clean boot to REPL with
conservation intact (resident_sum + reservoir == Q48_ONE) on all three
architectures (amd64/aarch64/riscv64); BLOCK/BUFFER touches exercised
live from the REPL with no crash; a 22,000-distinct-block flood loop
against an artificially shrunk Stadium ran clean under heavy admission
load. A live MIGRATE console fire was not directly observed this
session (root-caused to a pre-existing reservoir-floor/density-eviction
interaction unrelated to this change, documented in FABRIC-3.md) --
flagged as an honest follow-up, not silently claimed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Implements the full design from the prior commit in one pass. resolve_lbn()
is the single choke point threaded through the ten public LBN-consuming
entry points (blk_get_buffer, blk_update, blk_flush, blk_is_allocated,
blk_mark_allocated, blk_mark_free, blk_is_valid, blk_get_meta, blk_set_meta,
plus blk_get_empty_buffer covered via delegation) -- an LBN->LBN redirect,
not a new storage allocator, since the LBN space is already unified across
every attached blkio_dev backend. VM window cache staleness across a
relocation reuses the existing blk_vm_check_epoch() mechanism from
Milestone 2h's hot-detach fix for free -- g.epoch bumps on relocation too.
Persistence lands in the same pass: two new uint32_t fields
(reloc_start/reloc_devblocks) appended after hdr_crc in blk_volume_meta_t,
carved from existing padding without moving any earlier field's byte
offset -- an old formatted volume's zeroed padding reads back as
reloc_devblocks=0 ("no reloc capacity"), gracefully, not a format-breaking
change. compute_totals_from_B() generalized to account for the new
reserved region. reloc_flush_to_disk()/reloc_load_from_disk() mirror the
BAM I/O functions' own absolute-devblock-addressing shape; the persisted
copy's owner is first_disk_slot() (already existed, already used for this
exact "which device is canonical" question by blk_get_volume_meta()).
blk_subsys_relocate_block() is a mechanical primitive only -- copies
content (staged through a local buffer, since obtaining the target's
blk_get_buffer() result can evict and invalidate the source's cache
pointer if they share a device), frees the source BAM entry, appends the
exception entry, bumps the epoch, flushes to disk. RELOCATE-BLOCK exposes
it to FORTH, no policy of its own (ACL's job, per this session's direction).
A first live-test attempt gave a false negative against disk/artemis.img
(predates reloc capacity, so relocation only ever existed in memory that
boot) -- traced to the test's own setup before being mistaken for a bug,
then re-verified correctly against a fresh volume (new fixture,
disk/artemis-reloc-test.img): relocated a RAMDRIVE block to the fresh
disk, confirmed live resolution through the redirect, then confirmed both
the redirect and the relocated content survived an abrupt QEMU kill and
full reboot. Also fixed three lingering "glibc" doc-comment
misattributions from Milestone 2h (the actual allocator is this kernel's
own kmalloc) that survived an earlier FABRIC-2.md-only correction. All
three architectures re-verified clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
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
blk_subsys_detach_device() (block_subsystem.c) walks the device chain,
refuses removal of anything but the current tail (a mid-chain removal
would corrupt every later slot's start_lbn -- this architecture's own doc
already argues USB stays last specifically to avoid that), unlinks,
shrinks total_user_lbn, closes and frees the slot. Discards rather than
flushes dirty state -- the device is physically gone by the time this
runs (PORTSC disconnect only). Trigger wiring mirrors the attach path:
bot_msc_attached (set only once attach actually succeeds) gates a new
bot_msc_detach_pending flag set at PORTSC disconnect (not Disable Slot
completion, which is conditionally skipped and would miss concurrent
connect/disconnect pairs), consumed in sk_repl_idle().
Advisor flagged the real hazard ahead of time: block_words.c's VM block
window (blk_vm_lbn[]/blk_vm_cbuf[]) can go stale across a detach then a
same-LBN re-attach, and suggested a pointer-identity re-check in
blk_vm_load() as a minimal fix. That fix was implemented, then directly
falsified by its own designed-for-this test: attach a blank device, read
a block (populating the cache), detach, re-attach a device with distinct
content at the identical LBN, read again -- served stale content from
the first device. Root cause, confirmed live: glibc's allocator hands
free(slot) straight back to the very next same-size calloc(), so the
"fresh" and stale pointers were bitwise identical despite being two
different devices. Fixed properly with a monotonic blk_subsys_epoch()
counter (bumped on every attach/detach) checked by a new
blk_vm_check_epoch() helper at the one choke point (blk_vm_find(), plus
blk_vm_flush_all() which reads the same arrays directly) that covers
every path touching the window cache -- unfooled by address reuse.
Verified live with a new disk/usb-thumbdrive-test2.img fixture (distinct
content from the existing blank test image): attach A, read (cache hit
populated), detach, re-attach B at the same LBN, read again -- correctly
ran a fresh device read and returned B's real content, not A's stale
cached zeros. The failing pointer-comparison attempt's own capture log
kept as evidence, not deleted. All three architectures re-verified clean.
FABRIC-2.md Section X 2h marked complete -- enumeration through
hot-detach all live and verified; only WRITE(10) (2g's own still-open
item) remains unimplemented in the driver, not blocking anything here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
find and fix the real ACL-TTL measurement bug (zuse session never
authenticated, ACL enforcement never active)
Two mistakes corrected in sequence, both documented in full in
FABRIC-2.md Section R:
1. HEARTBEAT-TICKS@ was swapped to read heartbeat_ticks() -- a newer,
kernel-only ISR hardware-timer counter (src/starkernel/heartbeat.c,
the M5 TIME-TRUST engine) -- based on a misreading of which counter
"the one clock" law refers to. Reverted to vm->heartbeat.tick_count,
Loop #7 "Adaptive Heartrate", the actual year-plus-old counter the
whole physics runtime is built on. Removed the now-irrelevant
HEARTBEAT-PERIOD-NS@ accessor added to diagnose the wrong counter's
adaptive re-arm period. Three-arch QEMU re-acceptance: POST 1012/0/0
on amd64/aarch64/riscv64, HEARTBEAT-TICKS@ confirmed returning 77
(matching the original pre-heartbeat_ticks() acceptance) on all three.
2. The real bug, found after the revert: every "ACL enabled" measurement
in this investigation (Section P's 18-cell campaign, Section Q's
pilot) loaded ACL.4th and ran EXEC-DOE from the bare `ok>` prompt
without ever authenticating a zuse session. repl.c:303 keeps
emergency_console=1 until zuse_session=1; vm_core.c:755 skips the
entire ACL check block (TTL decrement and acl_recheck()) whenever
emergency_console is set. ACL was configured but never armed.
capsules/zuse.4th's pre-existing self-pin bug means the documented
automatic zuse activation doesn't work either (still flagged, not
fixed) -- worked around by invoking the directly-registered
ZUSE-AUTHENTICATE word explicitly.
Validated pilot (amd64, seed 12345, 30 reps, same build, disabled vs.
genuinely zuse-authenticated-enabled): +117 ticks, +0.0448% overhead.
Disabled-arm determinism double-confirmed (261064 ticks, exact repeat
on a fresh boot) -- the 117-tick difference is real signal, not noise.
Reconciles with the original ACL-RWT campaign's own heartbeat-tick
result (+0.0054%-0.0088%, same order of magnitude). Section P's
wall-clock numbers and Section Q's "instrument blind" conclusion are
both marked invalidated/corrected in place, not deleted.
n=1 per arm, one architecture -- not yet a full campaign. Scoped as
next step, not undertaken in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vm->heartbeat.tick_count (FORTH-dispatch counter)
Captain Bob's law is unambiguous: the adaptive heartbeat is the one and
only clock, full stop. The first cut of this word read the wrong
counter under that name -- vm->heartbeat.tick_count is a colon-word-
dispatch counter gated at a fixed cadence (frozen during idle, blind to
per-dispatch CPU cost, see FABRIC-2.md Section Q). The real adaptive
heartbeat is heartbeat_ticks() in src/starkernel/heartbeat.c, driven
directly by the ISR-latched 100Hz hardware timer -- genuinely
time-based, confirmed advancing during idle wall-clock time on all
three architectures (amd64 4039->5510, aarch64 6126->7607, riscv64
2965->4466, each over ~15s idle). Kernel build only (__STARKERNEL__);
hosted build has no ISR timer and keeps the old fallback.
Three-arch QEMU acceptance: POST 1012/0/0 on each, word live-tested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The adaptive heartbeat tick counter (vm->heartbeat.tick_count) is the
project's sole canonical clock for timing measurements -- host wall-clock
is not a valid substitute. Exposes it read-only so DoE/overhead campaigns
can measure elapsed ticks instead of wall-clock deltas.
Verified: three-arch QEMU acceptance (amd64/aarch64/riscv64), POST
1012/0/0 on each, HEARTBEAT-TICKS@ live-tested returning a real non-zero
count on all three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FENCE ( -- ) exposes the dict_fence_latest/dict_fence_here state FORGET
already honored internally, letting callers (e.g. a future SDK capsule)
raise the boundary after loading their own content -- no new VM fields,
no policy logic beyond exposing existing state.
Writing a direct test for it surfaced a real, severe, pre-existing bug in
FORGET's relink logic, unrelated to FENCE itself and reproducible with the
original boot-time fence alone:
- Forgetting the single newest word incorrectly destroyed every other word
back to the fence too, not just the target.
- Forgetting an older word (correctly cascading to remove newer words too,
per FORTH-79 semantics) crashed with SIGSEGV.
Root cause: the relink code's target_prev pointer was, by construction,
always inside the range the preceding loop had just freed whenever target
wasn't vm->latest -- so writing through it was a use-after-free every time
that branch executed. Fixed by removing the target_prev tracking and both
branches entirely; vm->latest unconditionally becomes target_next (target's
own captured, still-valid link) after the free loop, correct in every case.
Added a FENCE test suite to dictionary_manipulation_words_test.c (Module 14)
including the exact regression case (forgetting the newest word must not
disturb an older one). Verified zero warnings and identical POST/dict_hash
results across all three kernel architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cluster 4 of the POST-coverage sweep: physics_freeze_words_test.c covers the 6
words proof/StarForth_Physics_Freeze_Words.thy actually gives real lemmas for
(FREEZE-WORD, UNFREEZE-WORD, FROZEN?, HEAT!, HEAT@, DECAY-RATE@), correcting
an earlier fork summary's wrong "5 words" scope.
Writing the tests surfaced two independent, pre-existing bugs in
physics_freeze_words.c, both now fixed:
- Every address-taking word cast the VM's caddr directly to a host pointer
instead of resolving it through vm_ptr() -- caddr is an offset into
vm->memory, not a host pointer. Fixed in all 9 call sites (the 5 in-scope
words plus SHOW-HEAT, which shares the identical pattern).
- Every underflow check used dsp < N (item count) instead of dsp < N-1, since
this VM's dsp is a 0-indexed top-of-stack pointer. Fixed in all 6 checks.
Together these meant every word in this file taking a stack-supplied name has
been broken for any real caller since the file was written. Verified zero
build warnings and a clean three-arch QEMU boot (amd64/aarch64/riscv64), 1009
passed / 0 failed / 0 errors identically on all three, dict_hash matching
across arches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Maintainability sweep (prompted by "this is getting hard to maintain"):
fixed the remaining three warning classes after the missing-field-
initializers commit -- 2x -Wsign-compare (control_words.c, cast at the
comparison site rather than changing cf_last_mode's type, which
deliberately holds a -999 sentinel outside vm_mode_t's valid range),
2x -Wstringop-truncation (mkcapsule.c, strncpy+manual-null-terminate
replaced with the idiomatic snprintf equivalent), and 26x
-Wunused-parameter (mostly documented stubs, silenced with the repo's
existing (void)param; idiom).
One of the unused-parameter warnings was not a deliberate stub -- a
real bug. restore_vm_state() (test_common.c) is named, documented, and
called by nine real call sites (acl_words_test.c x8 plus its own
internal use) as "restore saved VM state", but ignored all four of its
parameters and hard-reset to a fixed baseline instead, silently not
restoring what any caller actually saved. Fixed to actually assign the
passed-in dsp/rsp/error/mode. Found while fixing warnings, reported
before touching it, fixed/tested/documented/committed on explicit
instruction.
Verified: all three architectures build with zero C-compiler warnings
(amd64: 3040 -> 0; aarch64's one remaining note is lld-link's own
unrelated linker warning, not a C warning). Full amd64 acceptance boot
post-fix: POST 1003/965/0/0/38 (total/passed/failed/errors/stubs),
"ALL IMPLEMENTED TESTS PASSED!", contract checks (A4'/A1) all passed,
dict_hash=0x24b4279f0670aa3a -- an exact match to this document's own
previously-recorded baseline hash.
.claude/CLAUDE.md corrected to describe the real -Wno-error= exemption
list instead of the "-Wall -Werror" oversimplification. FABRIC-2.md
Section J records the full sweep, including doc-tree staleness findings
flagged but not fixed this pass (docs/lithosananke/ROADMAP.md branch
topology, docs/03-architecture/word-acl/DESIGN.md's Phase 7 claim
contradicting CLAUDE.md, top-level ROADMAP.md's stale StarForth-era
status, the Isabelle pipeline-metrics model mismatch).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DECAY-RATE@ (physics_freeze_words.c) pushed to the data stack with no
capacity check and no prior pop to make room, unlike its neighbors in
the same file -- the one live, unconditional missing-guard bug the
Isabelle sweep's ~15 candidate findings reduced to once checked against
vm_push()'s real internal bounds check (see proof/FINDINGS.md SS2).
Removed dictionary_manipulation_words.c's [ ] STATE and defining_words.c's
DEFER IS DEFER@ (plus the now-orphaned defining_runtime_defer helper) --
all confirmed permanently shadowed by later dictionary registrations
(defining_words.c and defer_words.c respectively), per FORTH's
newest-first lookup. No behavior change: the removed code was already
unreachable.
Verified: hosted `make` builds clean under -Wall -Werror; the hosted
self-test suite passes 965/965 implemented tests with no regressions.
Three-architecture QEMU acceptance boot, all clean to ok> with an
identical dict_hash=0x24b4279f0670aa3a across amd64/aarch64/riscv64 and
identical 1003/965/0/0 test totals -- logs attached.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
full-screen vt100 terminal
4.4v -- keyboard-to-REPL bridge, real and tested:
Refactored KEY-EVENT's per-arch translation logic (keyboard_words.c) into
a shared C function, sk_key_event_poll(), so the REPL bridge reuses item
4.3.5f's already-converged Linux-keycode-namespace event stream instead
of building separate amd64/aarch64/riscv64 tables. repl.c's sk_kbd_getc()
decodes the standard US-QWERTY printable range plus Enter/Backspace/Shift
against that stream; sk_readline() polls it as a second source alongside
console_getc(). Verified via QEMU monitor sendkey injection, and by
Captain Bob typing directly into the live QEMU window over real emulated
PS/2 hardware mid-session (1 1 + . -> 2 ok, then a clean BYE shutdown).
4.4ab -- simplify to a full-screen terminal:
Captain Bob's call, reverting the 640x480 CANVAS box + independent REPL
strip (4.4o/4.4t/4.4x/4.4z) in favor of the simplest shape: the entire
framebuffer is one vt100 terminal, g_vt.cols/rows = fb_width()/fb_height()
divided by cell size, no origin offset, no box, no strip, no border
drawing. The REPL prompt is just the terminal's last scrolling line.
Scrollback, TTF rendering, and SGR color are all box-agnostic and keep
working unmodified.
4.4r -- reframed as a text/graphics mode toggle:
"Hide/show the scroll box" stopped meaning anything once the box was
removed; the underlying need survives as a whole-screen mode switch.
vt100_toggle_graphics() is a two-state machine (VISIBLE/HIDDEN) -- hidden
mode stops the terminal from touching the framebuffer while its logical
state keeps advancing, so direct framebuffer/TTF-TEXT drawing can use the
whole screen; showing again wipes and reuses scrollback_redraw() to
restore the terminal exactly. Reachable two ways, one transition function:
physically via Alt+TAB (4.4y revised from Ctrl+TAB) and programmatically
via the new ALT+TAB FORTH word.
Verified: three-arch clean QEMU boot + logs; amd64 screendump confirms
full-width text with no box/strip artifacts.
Punch list §25 items 4.4v/4.4r/4.4ab complete; 4.4y revised.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scope decided with Captain Bob before implementation: ring buffer +
recall on today's full-screen vt100 grid, not also confining REPL text
to the 4.4o 640x480 box (that confinement stays open as its own future
item, not a third silent deferral). No keyboard input path exists yet
(M8 unstarted), so the trigger is two new FORTH words, SCROLL-BACK
( n -- ) / SCROLL-FWD ( n -- ), exercised via serial injection.
vt100.c gains a text-only 1000-line ring buffer (kmalloc'd, tens of KB
-- not pixel snapshots, which would be ~1000x larger for no benefit)
plus a shadow buffer mirroring the current screen. scroll_up() now
pushes evicted rows into the ring before the pixel scroll. History is
one continuous sequence (ring then shadow); scrolling always redraws
from that sequence -- no separate pixel-scroll path for scrollback,
decided up front to avoid retrofitting later.
New src/word_source/scroll_words.c (Module 31), thin wrappers over
console_fb_scroll_back()/_fwd() -> vt100_scroll_back()/_fwd(). Bug
caught during live testing: both words initially used an off-by-one
underflow check (dsp < 1) copied from a different, older dsp
convention elsewhere in this codebase; vm_pop() (which these words
actually call) uses dsp as a 0-based top-of-stack index, so the check
rejected every legitimate single-argument call. Fixed by removing the
separate precheck and relying on vm_pop()'s own guard.
Live-verified on all three architectures (exceeds this item's
amd64-minimum bar): generated 50+ lines via a FORTH loop, confirmed
SCROLL-BACK recovers correctly older content, and on amd64 confirmed
SCROLL-FWD returns to genuinely live state (not a frozen snapshot) by
showing the injected commands' own echo. Known limitation confirmed by
direct pixel measurement: redrawn lines lose their original SGR color
(not stored per-cell) -- text recovers exactly, color does not.
Three-arch verified: Failed: 0, dict-hashes identical across all
three (values changed correctly from prior items -- two new words
were added).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FABRIC.md: renumber Artemis-last boundary 4.4 -> 4.5, add new 4.4 REPL item
(console visual design scoped this session -- CANVAS is one shared region
above the REPL strip, 640x480 scroll box centered within it, stroke font,
ANSI color, VM-identity + ACL-identity prompt segments, ~1000-line
scrollback), linked to repl-mockup.png.
ROADMAP.md: mark M8 section OBSOLETE -- replaced by FABRIC.md 4.4, kept for
history only.
ttf_words.c: TTF-TEXT's y argument now Cartesian (origin bottom-left of the
physical framebuffer, y increasing upward) instead of raw framebuffer
coordinates, translated once via fb_height() - y. Verified via amd64
screendump (logs/20260811-070418/), text renders right-side-up with
increasing y moving up the screen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 4.3.7e complete. New src/word_source/ttf_words.c
registers TTF-TEXT ( c-addr u x y size color -- ): lazily loads the v1
default font capsule + raster cache once, decodes UTF-8 (C
reimplementation mirroring capsules/fabric.4th's DECODE-UTF8 exactly),
looks up each glyph's cached bitmap, blits via fb_put_pixel, advances
the pen by the glyph's real hmtx advance width scaled to pixels.
Necessary plumbing: ttf_parse() now also locates hhea/hmtx, and
ttf_glyph_advance_width() reads a glyph's advance width -- required for
this item's own "proportional spacing correct" acceptance clause, no
advance-width data existed anywhere else in the parser. Verified in
tools/ttftest.c: A/a/0/space all read advance_width=600, correctly
uniform since JetBrainsMono-Regular.ttf is monospace.
(x,y) is raster pixel space (top-left origin, Y-down), deliberately not
the stroke font TEXT's Cartesian Y-up convention -- recorded explicitly
in ttf_words.h, not conflated.
Verified live, amd64, screendump: injected
S" Hi 4.3.7e!" 200 200 28 16777215 TTF-TEXT over a serial socket after
boot, no error, captured a screendump showing the string rendered
legibly with correct mixed-case/digit/punctuation glyphs and even
spacing. TTF-TEXT is this item's permanent deliverable, not a
throwaway probe. Compile-checked clean on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 4.3.5f complete. Keyboard-input slice done.
New KEY-EVENT ( -- keycode pressed -1 | 0 ) converges amd64's i8042,
riscv64's and aarch64's virtio-keyboard-pci behind one shape. Translation
needed is minimal: XT Set-1 make codes and Linux input keycodes are
numerically identical across the standard 84-key block (a documented
historical property of the Linux input subsystem, confirmed against this
host's own headers and this tree's own prior live observations), so amd64
needs two lines, riscv64/aarch64 need none. Verified live with a real
keypress on all three architectures: identical "-1 1 30" output everywhere.
Three-arch acceptance boot clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 4.3.5e complete.
Extended virtio_input.c with a GIC-routed interrupt path alongside 4.3.5c's
PLIC one -- same capability walk, feature negotiation, eventq handling
(confirming §27.5.1's prediction that these items would share most of the
driver). aarch64_irq_handler() dispatches to virtio_input_isr() before its
EOIR write, same claim-dispatch-complete ordering riscv64 uses. Verified
with a real QEMU sendkey keypress: exact KEY_A/press match, two real
interrupts serviced, identical result to riscv64. Found (not fixed) an
unrelated pre-existing bug: BYE's cold-reset path faults on aarch64,
discovered incidentally since nobody had exercised it from a monitored
session before. Three-arch acceptance boot clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 4.3.5c complete.
Amended from a nonexistent MMIO transport to PCI (matching the board's
actual virtio-blk-pci precedent). New virtio-input driver: eventq with
pre-posted buffers, PLIC source computed at runtime from PCI slot/pin
(derived live from this host's QEMU riscv64 DTB), mandatory ISR-status
read, PCI interrupt-disable-bit check. New VKBD-EVENT/VKBD-DEBUG FORTH
words. Verified with a real QEMU sendkey keypress: exact KEY_A/press
match, two real interrupts serviced, zero exceptions. Three-arch
acceptance boot clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 4.3.5 complete.
New ioapic.c/i8042.c drivers (MADT-derived I/O APIC base, no hardcoded
constants) plus a KBD-SCAN/KBD-DEBUG diagnostic word pair. Three real
bugs found and fixed en route, all blocking this item's own acceptance:
a fatal LAPIC spurious-vector crash (nothing had driven a real external
interrupt through the I/O APIC before), OVMF leaving the keyboard device
itself scanning-disabled (0xF4 fix), and isr.S's stub table only having
individually-numbered stubs through vector 32 -- everything above that,
including our IRQ1 vector 33, silently reported as vector 255 regardless
of which IDT slot actually fired. Verified live via QEMU sendkey against
KBD-SCAN: correct XT Set-1 make/break codes for two different keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds q48_reduce_angle() (range-reduce a signed Q48.16 angle into
[-PI_Q48, PI_Q48] via one integer division plus a bounded fix-up loop) and
q48_sin_approx/q48_cos_approx (Taylor series, terms n=3,5,7,9,11 for sin
and n=2,4,6,8,10 for cos, early exit below 10). Q.SIN/Q.COS registered as
FORTH words in q48_words.c, same pattern as Q.LOG/Q.EXP/Q.SQRT.
Found mid-implementation: this codebase has two independent Q48.16
implementations -- src/word_source/q48_16_words.c (hosted/vendored) and
src/starkernel/math/q48_16.c (kernel-only; the kernel build does not
compile the former at all). The hosted build linked fine after the first
pass; the kernel build failed with undefined references until the same
two functions were added to both .c files and both q48_16.h headers
(include/q48_16.h and include/starkernel/q48_16.h). Not fixed at the root
-- Q.LOG/Q.EXP/Q.SQRT already had this same four-file duplication,
unremarked until now -- just navigated correctly for this item.
Verified live on amd64 via serial injection: sin/cos at 0, +-pi/2, pi, and
3pi (range-reduction across multiple turns) all match expected values
within Taylor-series truncation error (<0.2%).
All three architectures boot clean to ok> with the DoE completing;
dict_hash identical across all three (0x291a660b05fa7b52).
FABRIC.md item 4.3.3a marked done with full acceptance evidence.
Adds Module 28 (framebuffer_words.c/.h): PLOT ( x y color -- ), FB-WIDTH,
FB-HEIGHT -- raw hardware-boundary C primitives, kernel-only, no-op on
hosted builds, same pattern as every other module.
Adds capsules/fabric.4th (blocks 4900-4902, mkcapsule --lint clean):
COS45/Z->DELTA/PROJECT/CART-Y/CART-PLOT -- the 45-degree cavalier
orthographic projection and Y-flip, in FORTH per the compose-in-FORTH-first
rule (this is policy, not hardware access).
Found and fixed a second real bug while live-testing CART-PLOT over the
serial socket: defining_word_variable() (defining_words.c) captured
vm->here as a VARIABLE's address with no alignment call first, while
vm_load_cell/vm_store_cell require 8-byte-aligned addresses. This capsule's
VARIABLE ZD landed misaligned (945) purely by chance of what preceded it;
other capsules' variables happened to land aligned by luck, not guarantee.
Real deviation from FORTH-83/ANS, which specifies VARIABLE reserves an
aligned cell. Fixed with vm_align(vm) before capturing addr -- ALIGN
already existed as a word but VARIABLE wasn't calling it.
Verified end-to-end on amd64 via manual serial injection + QEMU screendump:
plotted 4 marker points (origin, +100 X, +100 Y, +50 Z) and confirmed all
landed at hand-calculated raster coordinates, including the diagonal
up-right shift for the Z-axis point -- the projection math is correct, not
just non-crashing. fb/fabric-test-cart-plot.png. 4.3.1's corner diagnostic
still renders correctly in the same shot, confirming no regression.
All three architectures (amd64/aarch64/riscv64) boot clean to ok> with the
DoE completing; dict_hash identical across all three
(0xc7f9adf885e306d2), confirming parity is unaffected.
FABRIC.md item 4.3.3 marked done with full acceptance evidence.
No shift primitive existed anywhere in the vendored VM word set. Adds
both as FORTH-83-extension words next to INVERT, guarded against stack
underflow and out-of-range shift counts (u >= 64).
Their absence was masking a real bug: capsules/hermes/init.4th's
CH-MINT-ID (item 4.2) calls LSHIFT to pack a 64-bit channel ID, which
was silently tripping the capsule loader's forward-reference retry
logic and splicing CH-REQUEST's body into CH-MINT-ID's definition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The generic block subsystem (blk_format_or_load_disk) auto-reformatted
any disk lacking its own low-level 'STFR' header at attach time, before
Artemis's Forth-level BLANK/LithosAnanke/Unrecognized classification
ever ran -- so ART-HALT-UNRECOG's "Disk preserved" message was false.
Split detection from commit: an unrecognized/blank disk is now left
PROVISIONAL (geometry computed in memory only, all writes refused)
until explicitly confirmed via the new blk_subsys_confirm_format() /
BLK-CONFIRM-FORMAT primitive. Artemis calls it from ART-FORMAT and
ART-RESUME, never from ART-HALT-UNRECOG.
Verified on amd64/aarch64/riscv64: parity intact (identical dict_hash),
normal recognized-disk resume + persist-read unaffected, and a
regenerated disk/artemis-unrecognized-test.img (the old copy had itself
been silently corrupted by this exact bug) now stays byte-for-byte
identical across a halted boot on amd64 and riscv64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ABORT is documented and tested in this codebase as standard FORTH-79
behavior -- system_words_test.c:63: "Should clear stacks and return to
QUIT" -- meaning it should unwind all the way back to the outermost
interpreter loop, abandoning whatever's left of the current line/block.
The implementation only unwound one level: every place that checked
vm->abort_requested cleared it the instant it saw it, so it never
survived to propagate past the first nested frame.
This surfaced via Artemis's ART-HALT-UNRECOG (capsules/artemis/init.4th):
on an unrecognized disk it correctly printed "ARTEMIS HALT: unrecognized
disk content" and called ABORT, but WELCOME (the next line in the same
block) ran anyway, and Artemis announced ready to Hermes and joined the
fleet normally -- contradicting .claude/ARTEMIS.md's "Refuse to mount...
do not overwrite it" requirement. Root cause is general, not
Artemis-specific, and present identically in both the hosted and kernel
VM cores.
Fixed at every level execution can nest through, verified by exhaustively
grepping every !vm->error-gated continuation loop and adding the parallel
!vm->abort_requested check:
- execute_colon_word (src/vm.c, src/starkernel/vm/vm_core.c): stop
clearing the flag on return -- every colon-word call is a recursive
call to this same function, so leaving it set lets every enclosing
frame's own check also unwind.
- vm_interpret (src/vm.c, src/starkernel/vm/vm_core.c): stop parsing
further words in the current input string once the flag is set.
- exec_block_with_retry (src/starkernel/capsule/capsule_loader.c):
capsule birth's line-by-line block executor -- stop processing further
lines in the current block, but return 0 (not -1), so
capsule_exec_payload still loads later blocks in the same capsule
payload. Returning -1 here would have silently broken word definitions
in blocks that come after the aborting one for reasons unrelated to
why it aborted (concretely, Artemis's ART-PING/LOAD-DOE in blocks
4851/4852, which follow the entry block 4133).
- THRU and --> (src/word_source/block_words.c): stop processing further
blocks/lines in their own loops.
- DODOES (src/word_source/defining_words.c): the CREATE...DOES> runtime
has its own hand-rolled execution loop, separate from
execute_colon_word -- same bug class, same fix. Also guarded the
post-loop "if (vm->rsp < base_rsp) vm->rsp = base_rsp" clamp so it
doesn't fire on an abort exit -- ABORT's own reset_vm_state() already
set rsp; restoring it to base_rsp would have partially undone that.
- Both REPL loops (src/repl.c, src/starkernel/repl.c x2 call sites):
clear the flag after each line, mirroring the existing vm->error
pattern, so a mid-line abort doesn't silently freeze subsequent
interactive input.
Verified directly: ": AB-TEST 1 2 3 ABORT 999 . ; AB-TEST 42 . CR
777 . CR" -- 999 never prints (stops mid-colon-word), 42 never prints
(stops the rest of the same line), 777 prints fine (next line
unaffected). Artemis: WELCOME/"Artemis ready" no longer fires after the
halt message. No regression: all three architectures still show PASS:
persist-read, PASS: E2E msg flow, and matching dict_hash on the normal
(non-aborted) boot path; hosted test suite 965 passed / 0 failed.
Known follow-up, not fixed here (see memory for details): Artemis still
announces ready to Hermes via a separate call path (CD-INIT, block 4141)
that never went through capsule_exec_payload's block chain in the first
place, and the disk file still picks up incidental writes even on a
correctly-halted boot -- likely generic block-subsystem housekeeping,
not traced yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses items #1 (partial) and #2 of
docs/working/archive/session-logs/2026-07-24-punch-list.md.
docs/lithosananke/hosted-acceptance-test/README.md:
- riscv64 leg used riscv64-linux-gnu-gcc, which fails to build this tree
(nanosleep visibility under -std=c99). Replaced with the working
clang-18 --target=riscv64-linux-gnu --sysroot=/usr/riscv64-linux-gnu
invocation, verified end-to-end.
- All three arch sections referenced a -c "<script>" flag that has never
existed in cli.c/main.c. Corrected to the working
`echo "..." | starforth -s` pattern, verified on all three architectures.
- Updated Prerequisites: qemu-user alone is sufficient (guest binaries are
static; qemu-user-static provides static *emulators*, not required here).
Source fixes (ported from the old pre-split monorepo's master, commit
4db9946a, where they were made but never carried over to this line):
- src/math_portable.c: `-100LL << 16` is UB (shifting a negative value)
under clang's -Wshift-negative-value; changed to `-(100LL << 16)`.
- src/physics_pipelining_metrics.c: removed dead q48_mul_q48()
(-Wunused-function under clang; GCC doesn't flag this by default).
- src/word_source/editor_words.c: removed dead set_scr() (same reason).
These three were required just to get the documented clang build to
compile at all. The punch list's higher-severity item — a genuine
SIGSEGV-causing register-reuse hazard in vm_pop_asm/vm_rpop_asm
(include/vm_asm_opt_riscv64.h) — is intentionally NOT included here; it's
a separate, more careful change and isn't required for this build to
succeed (latent only under clang; this repo's kernel build uses GCC).
Verified: all three hosted builds compile and run correctly (amd64
native, aarch64 via qemu-aarch64, riscv64 via qemu-riscv64), each
printing "3 Goodbye!" for the piped `1 2 + . BYE` script. All three
Makefile.starkernel builds (amd64/aarch64/riscv64) still compile cleanly
with these shared vendored-source changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>